enhance: export cmd reuses existing namespaces

This commit is contained in:
Gabriel Horner
2025-08-21 17:16:07 -04:00
parent 6a84e56984
commit 0722097df1
20 changed files with 288 additions and 1707 deletions

View File

@@ -72,7 +72,6 @@
frontend.commands commands frontend.commands commands
frontend.common.file-based.db common-file-db frontend.common.file-based.db common-file-db
frontend.common.date common-date frontend.common.date common-date
frontend.common.file.core common-file
frontend.common.file.util wfu frontend.common.file.util wfu
frontend.common.missionary-util c.m frontend.common.missionary-util c.m
frontend.common.schema-register sr frontend.common.schema-register sr
@@ -167,6 +166,9 @@
lambdaisland.glogi log lambdaisland.glogi log
logseq.cli.common.graph cli-common-graph logseq.cli.common.graph cli-common-graph
logseq.cli.text-util cli-text-util logseq.cli.text-util cli-text-util
logseq.cli.common.export.common cli-export-common
logseq.cli.common.export.text cli-export-text
logseq.cli.common.file common-file
logseq.common.config common-config logseq.common.config common-config
logseq.common.date-time-util date-time-util logseq.common.date-time-util date-time-util
logseq.common.graph common-graph logseq.common.graph common-graph

View File

@@ -14,7 +14,8 @@
logseq.cli.commands.graph cli-graph logseq.cli.commands.graph cli-graph
logseq.cli.common.graph cli-common-graph logseq.cli.common.graph cli-common-graph
logseq.cli.common.export.text cli-export-text logseq.cli.common.export.text cli-export-text
;; logseq.cli.common.export.common cli-export-common logseq.cli.common.export.common cli-export-common
logseq.cli.common.file common-file
logseq.cli.util cli-util logseq.cli.util cli-util
logseq.cli.text-util cli-text-util logseq.cli.text-util cli-text-util
logseq.common.config common-config logseq.common.config common-config

2
deps/cli/bb.edn vendored
View File

@@ -40,5 +40,5 @@
:tasks/config :tasks/config
{:large-vars {:large-vars
{:max-lines-count 30 {:max-lines-count 45
:metadata-exceptions #{:large-vars/cleanup-todo}}}} :metadata-exceptions #{:large-vars/cleanup-todo}}}}

View File

@@ -4,39 +4,38 @@
[cljs.pprint] [cljs.pprint]
[clojure.string :as string] [clojure.string :as string]
[datascript.core :as d] [datascript.core :as d]
[logseq.cli.common.export.text :as cli-export-text]
[logseq.cli.common.export.common :as cli-export-common] [logseq.cli.common.export.common :as cli-export-common]
[logseq.cli.common.file :as cli-common-file] [logseq.cli.common.export.text :as cli-export-text]
[logseq.cli.common.zip :as cli-common-zip] [logseq.cli.common.file :as common-file]
[logseq.cli.common.util :as cli-common-util]
[logseq.cli.util :as cli-util] [logseq.cli.util :as cli-util]
[logseq.common.config :as common-config] [logseq.common.config :as common-config]
[logseq.common.util :as common-util] [logseq.common.util :as common-util]
[logseq.db.common.entity-plus :as entity-plus] [logseq.db.common.sqlite-cli :as sqlite-cli]))
[logseq.db.common.sqlite-cli :as sqlite-cli]
[logseq.db.sqlite.create-graph :as sqlite-create-graph]))
(defn get-all-page->content (defn- get-content-config [db]
[repo db options] (let [repo-config (-> (d/q '[:find ?content
;; TODO: entity-plus or sqlite-util load faster? :where [?b :file/path "logseq/config.edn"] [?b :file/content ?content]]
(let [filter-fn (if (entity-plus/db-based-graph? db) db)
(fn [ent] ffirst
(or (not (:logseq.property/built-in? ent)) common-util/safe-read-map-string)
(contains? sqlite-create-graph/built-in-pages-names (:block/title ent)))) indent
(constantly true))] ;; Copy of state/get-export-bullet-indentation
(->> (d/datoms db :avet :block/name) (case (get repo-config :export/bullet-indentation :tab)
(map #(d/entity db (:e %))) :eight-spaces
(filter filter-fn) " "
(map (fn [e] :four-spaces
[(:block/title e) " "
(cli-common-file/block->content repo db (:block/uuid e) {} options)]))))) :two-spaces
" "
:tab
"\t")]
{:export-bullet-indentation indent}))
(defn <get-file-contents (defn- get-file-contents
[repo db suffix] "Modified version of export.common/<get-file-contents which doesn't have to deal with worker threads"
;; TODO: p/let [repo db content-config suffix]
(let [page->content (get-all-page->content repo (let [page->content (common-file/get-all-page->content repo db content-config)]
db
;; TODO: Indentation
{:export-bullet-indentation "\t"})]
(map (fn [[page-title content]] (map (fn [[page-title content]]
{:path (str page-title "." suffix) {:path (str page-title "." suffix)
:content content :content content
@@ -44,29 +43,30 @@
:format :markdown}) :format :markdown})
page->content))) page->content)))
(defn export-files-as-markdown (defn- export-files-as-markdown
"options see also `export-blocks-as-markdown`" "Modified version of handler.export.text/export-files-as-markdown for the CLI"
[repo files options] [repo files options]
(mapv (mapv
(fn [{:keys [path title content]}] (fn [{:keys [path title content]}]
[(or path title) (cli-export-text/export-helper repo content :markdown options)]) [(or path title) (cli-export-text/export-helper repo content :markdown options)])
files)) files))
(defn export-repo-as-markdown! (defn- export-repo-as-markdown!
"Modified version of handler.export.text/export-repo-as-markdown for the CLI"
[repo db] [repo db]
(let [files* (<get-file-contents repo db "md")] (let [content-config (get-content-config db)
files* (get-file-contents repo db content-config "md")]
(when (seq files*) (when (seq files*)
(let [files (binding [cli-export-common/*current-db* db (let [files (binding [cli-export-common/*current-db* db
cli-export-common/*current-repo* repo cli-export-common/*current-repo* repo
cli-export-common/*content-config* {:export-bullet-indentation "\t"}] cli-export-common/*content-config* content-config]
(export-files-as-markdown repo files* nil)) (export-files-as-markdown repo files* nil))
repo' (string/replace repo common-config/db-version-prefix "") repo' (string/replace repo common-config/db-version-prefix "")
zip-file-name (str repo' "_markdown_" (quot (common-util/time-ms) 1000))] zip-file-name (str repo' "_markdown_" (quot (common-util/time-ms) 1000))
(prn :files files) zip (cli-common-util/make-export-zip zip-file-name files)]
(let [zip (cli-common-zip/make-zip zip-file-name files repo')] (-> (.generateNodeStream zip #js {:streamFiles true :type "nodebuffer"})
(-> (.generateNodeStream zip #js {:streamFiles true :type "nodebuffer"}) (.pipe (fs/createWriteStream (str zip-file-name ".zip"))))
(.pipe (fs/createWriteStream (str zip-file-name ".zip")))) (println "Exported graph to" (str zip-file-name ".zip"))))))
(println "Exported graph to" (str zip-file-name ".zip")))))))
(defn export [{{:keys [graph]} :opts}] (defn export [{{:keys [graph]} :opts}]
(if (fs/existsSync (cli-util/get-graph-dir graph)) (if (fs/existsSync (cli-util/get-graph-dir graph))

View File

@@ -6,7 +6,8 @@
(:require [cljs.core.match :refer [match]] (:require [cljs.core.match :refer [match]]
[clojure.string :as string] [clojure.string :as string]
[datascript.core :as d] [datascript.core :as d]
[logseq.cli.common.file :as cli-common-file] [logseq.cli.common.file :as common-file]
[logseq.cli.common.util :as cli-common-util :refer-macros [removev concatv mapcatv]]
[logseq.common.util :as common-util] [logseq.common.util :as common-util]
[logseq.db :as ldb] [logseq.db :as ldb]
[logseq.graph-parser.mldoc :as gp-mldoc] [logseq.graph-parser.mldoc :as gp-mldoc]
@@ -51,40 +52,15 @@
:keep-only-level<=N :all :keep-only-level<=N :all
:newline-after-block false}}) :newline-after-block false}})
;; Fn workarounds ;; Global vars that are not explicitly passed in all fns
;; ============== ;; These vars must be bound in order to use most fns in this namespace
(def ^:dynamic *current-db* nil) (def ^:dynamic *current-db* nil)
(def ^:dynamic *current-repo* nil) (def ^:dynamic *current-repo* nil)
;; Config used by logseq.cli.common.file fns
(def ^:dynamic *content-config* nil) (def ^:dynamic *content-config* nil)
(defn get-block-by-uuid
[id]
(d/entity *current-db* [:block/uuid (if (uuid? id) id (uuid id))]))
(defn zero-pad
[n]
(if (< n 10)
(str "0" n)
(str n)))
(defmacro concatv
"Vector version of concat. non-lazy"
[& args]
`(vec (concat ~@args)))
(defmacro mapcatv
"Vector version of mapcat. non-lazy"
[f coll & colls]
`(vec (mapcat ~f ~coll ~@colls)))
(defmacro removev
"Vector version of remove. non-lazy"
[pred coll]
`(vec (remove ~pred ~coll)))
;;; internal utils ;;; internal utils
(defn- get-blocks-contents (defn ^:api get-blocks-contents
[repo root-block-uuid & {:keys [init-level] [repo root-block-uuid & {:keys [init-level]
:or {init-level 1}}] :or {init-level 1}}]
(let [block (d/entity *current-db* [:block/uuid root-block-uuid]) (let [block (d/entity *current-db* [:block/uuid root-block-uuid])
@@ -93,22 +69,16 @@
root-id (:block/uuid block') root-id (:block/uuid block')
blocks (ldb/get-block-and-children *current-db* root-id) blocks (ldb/get-block-and-children *current-db* root-id)
tree (otree/blocks->vec-tree repo *current-db* blocks root-id {:link link})] tree (otree/blocks->vec-tree repo *current-db* blocks root-id {:link link})]
(cli-common-file/tree->file-content *current-repo* *current-db* tree (common-file/tree->file-content *current-repo* *current-db* tree
{:init-level init-level :link link} {:init-level init-level :link link}
*content-config*))) *content-config*)))
;; (defn root-block-uuids->content
;; [repo root-block-uuids]
;; (let [contents (mapv (fn [id]
;; (get-blocks-contents repo id)) root-block-uuids)]
;; (string/join "\n" (mapv string/trim-newline contents))))
(declare remove-block-ast-pos Properties-block-ast?) (declare remove-block-ast-pos Properties-block-ast?)
(defn- block-uuid->ast (defn- block-uuid->ast
[block-uuid] [block-uuid]
(let [block (into {} (get-block-by-uuid block-uuid)) (let [block (into {} (d/entity *current-db* [:block/uuid block-uuid]))
content (cli-common-file/tree->file-content *current-repo* *current-db* [block] {:init-level 1} *content-config*) content (common-file/tree->file-content *current-repo* *current-db* [block] {:init-level 1} *content-config*)
format :markdown] format :markdown]
(when content (when content
(removev Properties-block-ast? (removev Properties-block-ast?
@@ -124,13 +94,9 @@
(mapv remove-block-ast-pos (mapv remove-block-ast-pos
(gp-mldoc/->edn *current-repo* content format)))))) (gp-mldoc/->edn *current-repo* content format))))))
(defn get-page-content (defn ^:api get-page-content
[page-uuid] [page-uuid]
(let [repo *current-repo* (common-file/block->content *current-repo* *current-db* page-uuid nil *content-config*))
db *current-db*]
(cli-common-file/block->content repo db page-uuid
nil
*content-config*)))
(defn- page-name->ast (defn- page-name->ast
[page-name] [page-name]
@@ -160,9 +126,9 @@
[inline-coll meta] [inline-coll meta]
(with-meta ["Paragraph" inline-coll] meta)) (with-meta ["Paragraph" inline-coll] meta))
;; ;;; internal utils (ends) ;;; internal utils (ends)
;; ;;; utils ;;; utils
(defn priority->string (defn priority->string
[priority] [priority]
@@ -184,8 +150,8 @@
repetition (if repetition repetition (if repetition
(str " " (repetition-to-string repetition)) (str " " (repetition-to-string repetition))
"") "")
hour (when hour (zero-pad hour)) hour (when hour (cli-common-util/zero-pad hour))
min (when min (zero-pad min)) min (when min (cli-common-util/zero-pad min))
time (cond time (cond
(and hour min) (and hour min)
(common-util/format " %s:%s" hour min) (common-util/format " %s:%s" hour min)
@@ -196,12 +162,13 @@
(common-util/format "%s%s-%s-%s %s%s%s%s" (common-util/format "%s%s-%s-%s %s%s%s%s"
open open
(str year) (str year)
(zero-pad month) (cli-common-util/zero-pad month)
(zero-pad day) (cli-common-util/zero-pad day)
wday wday
time time
repetition repetition
close))) close)))
(defn hashtag-value->string (defn hashtag-value->string
[inline-coll] [inline-coll]
(reduce str (reduce str
@@ -217,32 +184,9 @@
ast-content))) ast-content)))
inline-coll))) inline-coll)))
;; (defn <get-all-pages ;;; utils (ends)
;; [repo]
;; (state/<invoke-db-worker :thread-api/export-get-all-pages repo))
;; (defn <get-debug-datoms ;;; replace block-ref, block-embed, page-embed
;; [repo]
;; (state/<invoke-db-worker :thread-api/export-get-debug-datoms repo))
;; (defn <get-all-page->content
;; [repo options]
;; (state/<invoke-db-worker :thread-api/export-get-all-page->content repo options))
;; (defn <get-file-contents
;; [repo suffix]
;; (p/let [page->content (<get-all-page->content repo
;; {:export-bullet-indentation (state/get-export-bullet-indentation)})]
;; (clojure.core/map (fn [[page-title content]]
;; {:path (str page-title "." suffix)
;; :content content
;; :title page-title
;; :format :markdown})
;; page->content)))
;; ;;; utils (ends)
;; ;;; replace block-ref, block-embed, page-embed
(defn- replace-block-reference-in-heading (defn- replace-block-reference-in-heading
[{:keys [title] :as ast-content}] [{:keys [title] :as ast-content}]
@@ -534,7 +478,7 @@
(if (get-in *state* [:replace-ref-embed :block&page-embed-replaced?]) (if (get-in *state* [:replace-ref-embed :block&page-embed-replaced?])
(do (set! *state* (assoc-in *state* [:replace-ref-embed :block&page-embed-replaced?] false)) (do (set! *state* (assoc-in *state* [:replace-ref-embed :block&page-embed-replaced?] false))
(recur block-ast-coll result-block-ast-tcoll (recur block-ast-coll result-block-ast-tcoll
(vec (concat block-ast-coll-to-replace-references block-ast-coll-replaced)) (concatv block-ast-coll-to-replace-references block-ast-coll-replaced)
(vec other-block-asts-to-replace-embed))) (vec other-block-asts-to-replace-embed)))
(recur block-ast-coll (reduce conj! result-block-ast-tcoll block-ast-coll-replaced) (recur block-ast-coll (reduce conj! result-block-ast-tcoll block-ast-coll-replaced)
(vec block-ast-coll-to-replace-references) (vec other-block-asts-to-replace-embed)))) (vec block-ast-coll-to-replace-references) (vec other-block-asts-to-replace-embed))))
@@ -547,7 +491,7 @@
(conj block-ast-coll-to-replace-references block-ast) (conj block-ast-coll-to-replace-references block-ast)
(vec block-ast-coll-to-replace-embeds))))))) (vec block-ast-coll-to-replace-embeds)))))))
;; ;;; replace block-ref, block-embed, page-embed (ends) ;;; replace block-ref, block-embed, page-embed (ends)
(def remove-block-ast-pos (def remove-block-ast-pos
"[[ast-type ast-content] _pos] -> [ast-type ast-content]" "[[ast-type ast-content] _pos] -> [ast-type ast-content]"
@@ -595,7 +539,7 @@
:result-ast-tcoll :result-ast-tcoll
persistent!)) persistent!))
;; ;;; inline transformers ;;; inline transformers
(defn remove-emphasis (defn remove-emphasis
":mapcat-fns-on-inline-ast" ":mapcat-fns-on-inline-ast"
@@ -654,9 +598,9 @@
{:r [] :after-break-line? true} {:r [] :after-break-line? true}
inline-coll))) inline-coll)))
;; ;;; inline transformers (ends) ;;; inline transformers (ends)
;; ;;; walk on block-ast, apply inline transformers ;;; walk on block-ast, apply inline transformers
(defn- walk-block-ast-helper (defn- walk-block-ast-helper
[inline-coll map-fns-on-inline-ast mapcat-fns-on-inline-ast fns-on-inline-coll] [inline-coll map-fns-on-inline-ast mapcat-fns-on-inline-ast fns-on-inline-coll]
@@ -726,9 +670,9 @@
;; else ;; else
block-ast))) block-ast)))
;; ;;; walk on block-ast, apply inline transformers (ends) ;;; walk on block-ast, apply inline transformers (ends)
;; ;;; simple ast ;;; simple ast
(def simple-ast-malli-schema (def simple-ast-malli-schema
(mu/closed-schema (mu/closed-schema
[:or [:or
@@ -763,7 +707,7 @@
:indent (reduce str (concatv (repeat (:level simple-ast) "\t") :indent (reduce str (concatv (repeat (:level simple-ast) "\t")
(repeat (:extra-space-count simple-ast) " "))))) (repeat (:extra-space-count simple-ast) " ")))))
(defn- merge-adjacent-spaces&newlines (defn- ^:large-vars/cleanup-todo merge-adjacent-spaces&newlines
[simple-ast-coll] [simple-ast-coll]
(loop [r (transient []) (loop [r (transient [])
last-ast nil last-ast nil

View File

@@ -1,25 +1,15 @@
(ns logseq.cli.common.export.text (ns logseq.cli.common.export.text
"Common fns between frontend and CLI for exporting as markdown"
(:require [clojure.string :as string] (:require [clojure.string :as string]
[logseq.cli.common.export.common :as common :refer [logseq.cli.common.export.common :as cli-export-common :refer
[*state* newline* indent raw-text space simple-asts->string]] [*state* newline* indent raw-text space simple-asts->string]]
[logseq.graph-parser.mldoc :as gp-mldoc])) [logseq.cli.common.util :refer-macros [removev concatv mapcatv]]
[logseq.graph-parser.mldoc :as gp-mldoc]
[logseq.graph-parser.schema.mldoc :as mldoc-schema]))
(defmacro concatv ;;; block-ast, inline-ast -> simple-ast
"Vector version of concat. non-lazy"
[& args]
`(vec (concat ~@args)))
(defmacro mapcatv (defn ^:api indent-with-2-spaces
"Vector version of mapcat. non-lazy"
[f coll & colls]
`(vec (mapcat ~f ~coll ~@colls)))
(defmacro removev
"Vector version of remove. non-lazy"
[pred coll]
`(vec (remove ~pred ~coll)))
(defn indent-with-2-spaces
"also consider (get-in *state* [:export-options :indent-style])" "also consider (get-in *state* [:export-options :indent-style])"
[level] [level]
(let [indent-style (get-in *state* [:export-options :indent-style])] (let [indent-style (get-in *state* [:export-options :indent-style])]
@@ -34,7 +24,7 @@
(defn- block-heading (defn- block-heading
[{:keys [title _tags marker level _numbering priority _anchor _meta _unordered size]}] [{:keys [title _tags marker level _numbering priority _anchor _meta _unordered size]}]
(let [indent-style (get-in *state* [:export-options :indent-style]) (let [indent-style (get-in *state* [:export-options :indent-style])
priority* (and priority (raw-text (common/priority->string priority))) priority* (and priority (raw-text (cli-export-common/priority->string priority)))
heading* (if (= indent-style "dashes") heading* (if (= indent-style "dashes")
[(indent (dec level) 0) (raw-text "-")] [(indent (dec level) 0) (raw-text "-")]
[(indent (dec level) 0)]) [(indent (dec level) 0)])
@@ -293,13 +283,13 @@
[ast-content] [ast-content]
(let [[type timestamp-content] ast-content] (let [[type timestamp-content] ast-content]
(-> (case type (-> (case type
"Scheduled" ["SCHEDULED: " (common/timestamp-to-string timestamp-content)] "Scheduled" ["SCHEDULED: " (cli-export-common/timestamp-to-string timestamp-content)]
"Deadline" ["DEADLINE: " (common/timestamp-to-string timestamp-content)] "Deadline" ["DEADLINE: " (cli-export-common/timestamp-to-string timestamp-content)]
"Date" [(common/timestamp-to-string timestamp-content)] "Date" [(cli-export-common/timestamp-to-string timestamp-content)]
"Closed" ["CLOSED: " (common/timestamp-to-string timestamp-content)] "Closed" ["CLOSED: " (cli-export-common/timestamp-to-string timestamp-content)]
"Clock" ["CLOCK: " (common/timestamp-to-string (second timestamp-content))] "Clock" ["CLOCK: " (cli-export-common/timestamp-to-string (second timestamp-content))]
"Range" (let [{:keys [start stop]} timestamp-content] "Range" (let [{:keys [start stop]} timestamp-content]
[(str (common/timestamp-to-string start) "--" (common/timestamp-to-string stop))])) [(str (cli-export-common/timestamp-to-string start) "--" (cli-export-common/timestamp-to-string stop))]))
string/join string/join
raw-text raw-text
vector))) vector)))
@@ -344,9 +334,9 @@
(when (> current-level 1) (when (> current-level 1)
(indent-with-2-spaces (dec current-level)))))]) (indent-with-2-spaces (dec current-level)))))])
;; {:malli/schema ...} only works on public vars, so use m/=> here ;; {:malli/schema ...} only works on public vars so make this public
;; (m/=> block-ast->simple-ast [:=> [:cat mldoc-schema/block-ast-schema] [:sequential simple-ast-malli-schema]]) (defn ^:large-vars/cleanup-todo ^:api block-ast->simple-ast
(defn- block-ast->simple-ast {:malli/schema [:=> [:cat mldoc-schema/block-ast-schema] [:sequential cli-export-common/simple-ast-malli-schema]]}
[block] [block]
(let [newline-after-block? (get-in *state* [:export-options :newline-after-block])] (let [newline-after-block? (get-in *state* [:export-options :newline-after-block])]
(removev (removev
@@ -406,7 +396,7 @@
(block-hiccup ast-content) (block-hiccup ast-content)
(assert false (print-str :block-ast->simple-ast ast-type "not implemented yet"))))))) (assert false (print-str :block-ast->simple-ast ast-type "not implemented yet")))))))
(defn- inline-ast->simple-ast (defn- ^:large-vars/cleanup-todo inline-ast->simple-ast
[inline] [inline]
(let [[ast-type ast-content] inline] (let [[ast-type ast-content] inline]
(case ast-type (case ast-type
@@ -419,7 +409,7 @@
"Code" "Code"
[(raw-text "`" ast-content "`")] [(raw-text "`" ast-content "`")]
"Tag" "Tag"
[(raw-text (str "#" (common/hashtag-value->string ast-content)))] [(raw-text (str "#" (cli-export-common/hashtag-value->string ast-content)))]
"Spaces" ; what's this ast-type for ? "Spaces" ; what's this ast-type for ?
nil nil
"Plain" "Plain"
@@ -458,8 +448,9 @@
nil nil
(assert false (print-str :inline-ast->simple-ast ast-type "not implemented yet"))))) (assert false (print-str :inline-ast->simple-ast ast-type "not implemented yet")))))
;;; block-ast, inline-ast -> simple-ast (ends)
(defn export-helper (defn ^:large-vars/cleanup-todo export-helper
[repo content format options] [repo content format options]
(let [remove-options (set (:remove-options options)) (let [remove-options (set (:remove-options options))
other-options (:other-options options)] other-options (:other-options options)]
@@ -473,30 +464,30 @@
:keep-only-level<=N (:keep-only-level<=N other-options) :keep-only-level<=N (:keep-only-level<=N other-options)
:newline-after-block (:newline-after-block other-options)}})] :newline-after-block (:newline-after-block other-options)}})]
(let [ast (gp-mldoc/->edn repo content format) (let [ast (gp-mldoc/->edn repo content format)
ast (mapv common/remove-block-ast-pos ast) ast (mapv cli-export-common/remove-block-ast-pos ast)
ast (vec (remove common/Properties-block-ast? ast)) ast (removev cli-export-common/Properties-block-ast? ast)
ast* (common/replace-block&page-reference&embed ast) ast* (cli-export-common/replace-block&page-reference&embed ast)
keep-level<=n (get-in *state* [:export-options :keep-only-level<=N]) keep-level<=n (get-in *state* [:export-options :keep-only-level<=N])
ast* (if (pos? keep-level<=n) ast* (if (pos? keep-level<=n)
(common/keep-only-level<=n ast* keep-level<=n) (cli-export-common/keep-only-level<=n ast* keep-level<=n)
ast*) ast*)
ast** (if (= "no-indent" (get-in *state* [:export-options :indent-style])) ast** (if (= "no-indent" (get-in *state* [:export-options :indent-style]))
(mapv common/replace-Heading-with-Paragraph ast*) (mapv cli-export-common/replace-Heading-with-Paragraph ast*)
ast*) ast*)
config-for-walk-block-ast (cond-> {} config-for-walk-block-ast (cond-> {}
(get-in *state* [:export-options :remove-emphasis?]) (get-in *state* [:export-options :remove-emphasis?])
(update :mapcat-fns-on-inline-ast conj common/remove-emphasis) (update :mapcat-fns-on-inline-ast conj cli-export-common/remove-emphasis)
(get-in *state* [:export-options :remove-page-ref-brackets?]) (get-in *state* [:export-options :remove-page-ref-brackets?])
(update :map-fns-on-inline-ast conj common/remove-page-ref-brackets) (update :map-fns-on-inline-ast conj cli-export-common/remove-page-ref-brackets)
(get-in *state* [:export-options :remove-tags?]) (get-in *state* [:export-options :remove-tags?])
(update :mapcat-fns-on-inline-ast conj common/remove-tags) (update :mapcat-fns-on-inline-ast conj cli-export-common/remove-tags)
(= "no-indent" (get-in *state* [:export-options :indent-style])) (= "no-indent" (get-in *state* [:export-options :indent-style]))
(update :fns-on-inline-coll conj common/remove-prefix-spaces-in-Plain)) (update :fns-on-inline-coll conj cli-export-common/remove-prefix-spaces-in-Plain))
ast*** (if-not (empty? config-for-walk-block-ast) ast*** (if-not (empty? config-for-walk-block-ast)
(mapv (partial common/walk-block-ast config-for-walk-block-ast) ast**) (mapv (partial cli-export-common/walk-block-ast config-for-walk-block-ast) ast**)
ast**) ast**)
simple-asts (vec (mapcat block-ast->simple-ast ast***))] simple-asts (mapcatv block-ast->simple-ast ast***)]
(simple-asts->string simple-asts))))) (simple-asts->string simple-asts)))))

View File

@@ -1,11 +1,12 @@
(ns logseq.cli.common.file (ns logseq.cli.common.file
"Convert blocks to file content. Used for exports and saving file to disk. Shared "Convert blocks to file content for file and DB graphs. Used for exports and
by CLI, worker and frontend namespaces" saving file to disk. Shared by CLI, worker and frontend namespaces"
(:require [clojure.string :as string] (:require [clojure.string :as string]
[datascript.core :as d] [datascript.core :as d]
[logseq.db :as ldb] [logseq.db :as ldb]
[logseq.db.common.entity-plus :as entity-plus] [logseq.db.common.entity-plus :as entity-plus]
[logseq.db.frontend.content :as db-content] [logseq.db.frontend.content :as db-content]
[logseq.db.sqlite.create-graph :as sqlite-create-graph]
[logseq.db.sqlite.util :as sqlite-util] [logseq.db.sqlite.util :as sqlite-util]
[logseq.graph-parser.property :as gp-property] [logseq.graph-parser.property :as gp-property]
[logseq.outliner.tree :as otree])) [logseq.outliner.tree :as otree]))
@@ -29,7 +30,7 @@
:else :else
content)) content))
(defn- transform-content (defn- ^:large-vars/cleanup-todo transform-content
[repo db {:block/keys [collapsed? format pre-block? properties] :as b} level {:keys [heading-to-list?]} context {:keys [db-based?]}] [repo db {:block/keys [collapsed? format pre-block? properties] :as b} level {:keys [heading-to-list?]} context {:keys [db-based?]}]
(let [title (or (:block/raw-title b) (:block/title b)) (let [title (or (:block/raw-title b) (:block/title b))
block-ref-not-saved? (and (not db-based?) block-ref-not-saved? (and (not db-based?)
@@ -121,3 +122,18 @@
(tree->file-content repo db tree (tree->file-content repo db tree
(assoc tree->file-opts :init-level init-level) (assoc tree->file-opts :init-level init-level)
context))) context)))
(defn get-all-page->content
"Exports a graph's pages as tuples of page name and page content"
[repo db options]
(let [filter-fn (if (ldb/db-based-graph? db)
(fn [ent]
(or (not (:logseq.property/built-in? ent))
(contains? sqlite-create-graph/built-in-pages-names (:block/title ent))))
(constantly true))]
(->> (d/datoms db :avet :block/name)
(map #(d/entity db (:e %)))
(filter filter-fn)
(map (fn [e]
[(:block/title e)
(block->content repo db (:block/uuid e) {} options)])))))

View File

@@ -7,7 +7,7 @@
[logseq.common.config :as common-config] [logseq.common.config :as common-config]
[logseq.common.graph :as common-graph])) [logseq.common.graph :as common-graph]))
(defn- graph-name->path (defn ^:api graph-name->path
[graph-name] [graph-name]
(when graph-name (when graph-name
(-> graph-name (-> graph-name

View File

@@ -0,0 +1,42 @@
(ns logseq.cli.common.util
"Common util fns between CLI and frontend"
(:require #?(:org.babashka/nbb ["jszip$default" :as JSZip]
:cljs ["jszip" :as JSZip])
#_:clj-kondo/ignore
[clojure.string :as string]))
#?(:cljs
(defn make-export-zip
"Makes a zipfile for an exported version of graph. Removes files with blank content"
[zip-filename file-name-content]
(let [zip (JSZip.)
folder (.folder zip zip-filename)]
(doseq [[file-name content] file-name-content]
(when-not (string/blank? content)
(.file folder (-> file-name
(string/replace #"^/+" ""))
content)))
zip)))
(defn zero-pad
[n]
(if (< n 10)
(str "0" n)
(str n)))
;; Macros are defined at top-level for frontend and nbb
(defmacro concatv
"Vector version of concat. non-lazy"
[& args]
`(vec (concat ~@args)))
(defmacro mapcatv
"Vector version of mapcat. non-lazy"
[f coll & colls]
`(vec (mapcat ~f ~coll ~@colls)))
(defmacro removev
"Vector version of remove. non-lazy"
[pred coll]
`(vec (remove ~pred ~coll)))

View File

@@ -1,22 +0,0 @@
(ns logseq.cli.common.zip
;; TODO: nbb
(:require ["jszip$default" :as JSZip]
[clojure.string :as string]))
(defn make-file [content file-name args]
(let [blob-content (clj->js [content])
last-modified (or (aget content "lastModified") (js/Date.))
args (clj->js args)]
(aset args "lastModified" last-modified)
(js/File. blob-content file-name args)))
;; TODO: reuse
(defn make-zip [zip-filename file-name-content _repo]
(let [zip (JSZip.)
folder (.folder zip zip-filename)]
(doseq [[file-name content] file-name-content]
(when-not (string/blank? content)
(.file folder (-> file-name
(string/replace #"^/+" ""))
content)))
zip))

View File

@@ -1,122 +0,0 @@
(ns frontend.common.file.core
"Convert blocks to file content. Used for exports and saving file to disk. Shared
by worker and frontend namespaces"
(:require [clojure.string :as string]
[datascript.core :as d]
[logseq.db :as ldb]
[logseq.db.common.entity-plus :as entity-plus]
[logseq.db.frontend.content :as db-content]
[logseq.db.sqlite.util :as sqlite-util]
[logseq.graph-parser.property :as gp-property]
[logseq.outliner.tree :as otree]))
(defn- indented-block-content
[content spaces-tabs]
(let [lines (string/split-lines content)]
(string/join (str "\n" spaces-tabs) lines)))
(defn- content-with-collapsed-state
"Only accept nake content (without any indentation)"
[repo format content collapsed?]
(cond
collapsed?
(gp-property/insert-property repo format content :collapsed true)
;; Don't check properties. Collapsed is an internal state log as property in file, but not counted into properties
(false? collapsed?)
(gp-property/remove-property format :collapsed content)
:else
content))
(defn- transform-content
[repo db {:block/keys [collapsed? format pre-block? properties] :as b} level {:keys [heading-to-list?]} context {:keys [db-based?]}]
(let [title (or (:block/raw-title b) (:block/title b))
block-ref-not-saved? (and (not db-based?)
(first (:block/_refs (d/entity db (:db/id b))))
(not (string/includes? title (str (:block/uuid b)))))
heading (:heading properties)
title (if db-based?
;; replace [[uuid]] with block's content
(db-content/recur-replace-uuid-in-block-title (d/entity db (:db/id b)))
title)
content (or title "")
content (cond
pre-block?
(let [content (string/trim content)]
(str content "\n"))
:else
(let [[prefix spaces-tabs]
(cond
(= format :org)
[(->>
(repeat level "*")
(apply str)) ""]
:else
(let [level (if (and heading-to-list? heading)
(if (> heading 1)
(dec heading)
heading)
level)
spaces-tabs (->>
(repeat (dec level) (:export-bullet-indentation context))
(apply str))]
[(str spaces-tabs "-") (str spaces-tabs " ")]))
content (if heading-to-list?
(-> (string/replace content #"^\s?#+\s+" "")
(string/replace #"^\s?#+\s?$" ""))
content)
content (if db-based? content (content-with-collapsed-state repo format content collapsed?))
new-content (indented-block-content (string/trim content) spaces-tabs)
sep (if (string/blank? new-content)
""
" ")]
(str prefix sep new-content)))]
(if block-ref-not-saved?
(gp-property/insert-property repo format content :id (str (:block/uuid b)))
content)))
(defn- tree->file-content-aux
[repo db tree {:keys [init-level link] :as opts} context]
(let [db-based? (sqlite-util/db-based-graph? repo)
block-contents (transient [])]
(loop [[f & r] tree level init-level]
(if (nil? f)
(->> block-contents persistent! flatten (remove nil?))
(let [page? (nil? (:block/page f))
content (if (and page? (not link)) nil (transform-content repo db f level opts context {:db-based? db-based?}))
new-content
(if-let [children (seq (:block/children f))]
(cons content (tree->file-content-aux repo db children {:init-level (inc level)} context))
[content])]
(conj! block-contents new-content)
(recur r level))))))
(defn tree->file-content
"Used by both file and DB graphs for export and for file-graph specific features"
[repo db tree opts context]
(->> (tree->file-content-aux repo db tree opts context) (string/join "\n")))
(defn- update-block-content
[db item eid]
;; This may not be needed if this becomes a file-graph only context
(if (entity-plus/db-based-graph? db)
(db-content/update-block-content db item eid)
item))
(defn block->content
"Converts a block including its children (recursively) to plain-text."
[repo db root-block-uuid tree->file-opts context]
(assert (uuid? root-block-uuid))
(let [init-level (or (:init-level tree->file-opts)
(if (ldb/page? (d/entity db [:block/uuid root-block-uuid]))
0
1))
blocks (->> (d/pull-many db '[*] (keep :db/id (ldb/get-block-and-children db root-block-uuid)))
(map #(update-block-content db % (:db/id %))))
tree (otree/blocks->vec-tree repo db blocks (str root-block-uuid))]
(tree->file-content repo db tree
(assoc tree->file-opts :init-level init-level)
context)))

View File

@@ -1,6 +1,5 @@
(ns frontend.extensions.zip (ns frontend.extensions.zip
(:require [clojure.string :as string] (:require [logseq.cli.common.util :as cli-common-util]
["jszip" :as JSZip]
[promesa.core :as p])) [promesa.core :as p]))
(defn make-file [content file-name args] (defn make-file [content file-name args]
@@ -11,12 +10,6 @@
(js/File. blob-content file-name args))) (js/File. blob-content file-name args)))
(defn make-zip [zip-filename file-name-content _repo] (defn make-zip [zip-filename file-name-content _repo]
(let [zip (JSZip.) (let [zip (cli-common-util/make-export-zip zip-filename file-name-content)]
folder (.folder zip zip-filename)]
(doseq [[file-name content] file-name-content]
(when-not (string/blank? content)
(.file folder (-> file-name
(string/replace #"^/+" ""))
content)))
(p/let [zip-blob (.generateAsync zip #js {:type "blob"})] (p/let [zip-blob (.generateAsync zip #js {:type "blob"})]
(make-file zip-blob (str zip-filename ".zip") {:type "application/zip"})))) (make-file zip-blob (str zip-filename ".zip") {:type "application/zip"}))))

View File

@@ -3,189 +3,34 @@
exclude some fns which produce lazy-seq, which can cause strange behaviors exclude some fns which produce lazy-seq, which can cause strange behaviors
when use together with dynamic var." when use together with dynamic var."
(:refer-clojure :exclude [map filter mapcat concat remove]) (:refer-clojure :exclude [map filter mapcat concat remove])
(:require [cljs.core.match :refer [match]] (:require [clojure.string :as string]
[clojure.string :as string] [frontend.db.conn :as conn]
[frontend.common.file.core :as common-file]
[frontend.db :as db]
[frontend.format.mldoc :as mldoc]
[frontend.modules.file.core :as outliner-file]
[frontend.modules.outliner.tree :as outliner-tree]
[frontend.state :as state] [frontend.state :as state]
[frontend.util :as util :refer [concatv mapcatv removev]] [logseq.cli.common.export.common :as cli-export-common]
[malli.core :as m]
[malli.util :as mu]
[promesa.core :as p])) [promesa.core :as p]))
;;; TODO: split frontend.handler.export.text related states (defn get-content-config []
(def ^:dynamic *state* {:export-bullet-indentation (state/get-export-bullet-indentation)})
"dynamic var, state used for exporting"
{;; current level of Heading, start from 1(same as mldoc), use when `block-ast->simple-ast`
:current-level 1
;; emphasis symbol (use when `block-ast->simple-ast`)
:outside-em-symbol nil
;; (use when `block-ast->simple-ast`)
:indent-after-break-line? false
;; TODO: :last-empty-heading? false
;; current: | want:
;; - | - xxx
;; xxx | yyy
;; yyy |
;; this submap is used when replace block-reference, block-embed, page-embed
:replace-ref-embed
{;; start from 1
:current-level 1
:block-ref-replaced? false
:block&page-embed-replaced? false}
;; submap for :newline-after-block internal state
:newline-after-block
{:current-block-is-first-heading-block? true}
;; export-options submap
:export-options
{;; dashes, spaces, no-indent
:indent-style "dashes"
:remove-page-ref-brackets? false
:remove-emphasis? false
:remove-tags? false
:remove-properties? true
:keep-only-level<=N :all
:newline-after-block false}})
;;; internal utils
(defn- get-blocks-contents
[repo root-block-uuid & {:keys [init-level]
:or {init-level 1}}]
(let [block (db/entity [:block/uuid root-block-uuid])
link (:block/link block)
block' (or link block)
root-id (:block/uuid block')
blocks (db/get-block-and-children repo root-id)]
(-> (outliner-tree/blocks->vec-tree repo blocks root-id {:link link})
(outliner-file/tree->file-content {:init-level init-level
:link link}))))
(defn root-block-uuids->content (defn root-block-uuids->content
"Converts given block uuids to content for given repo"
[repo root-block-uuids] [repo root-block-uuids]
(let [contents (mapv (fn [id] (binding [cli-export-common/*current-repo* repo
(get-blocks-contents repo id)) root-block-uuids)] cli-export-common/*current-db* (conn/get-db repo)
(string/join "\n" (mapv string/trim-newline contents)))) cli-export-common/*content-config* (get-content-config)]
(let [contents (mapv (fn [id]
(declare remove-block-ast-pos Properties-block-ast?) (cli-export-common/get-blocks-contents repo id)) root-block-uuids)]
(string/join "\n" (mapv string/trim-newline contents)))))
(defn- block-uuid->ast
[block-uuid]
(let [block (into {} (db/get-block-by-uuid block-uuid))
content (outliner-file/tree->file-content [block] {:init-level 1})
format :markdown]
(when content
(removev Properties-block-ast?
(mapv remove-block-ast-pos
(mldoc/->edn content format))))))
(defn- block-uuid->ast-with-children
[block-uuid]
(let [content (get-blocks-contents (state/get-current-repo) block-uuid)
format :markdown]
(when content
(removev Properties-block-ast?
(mapv remove-block-ast-pos
(mldoc/->edn content format))))))
(defn get-page-content (defn get-page-content
"Gets page content for current repo, db and state"
[page-uuid] [page-uuid]
(let [repo (state/get-current-repo) (binding [cli-export-common/*current-repo* (state/get-current-repo)
db (db/get-db repo)] cli-export-common/*current-db* (conn/get-db (state/get-current-repo))
(common-file/block->content repo db page-uuid cli-export-common/*content-config* (get-content-config)]
nil (cli-export-common/get-page-content page-uuid)))
{:export-bullet-indentation (state/get-export-bullet-indentation)})))
(defn- page-name->ast
[page-name]
(let [page (db/get-page page-name)]
(when-let [content (get-page-content (:block/uuid page))]
(when content
(let [format :markdown]
(removev Properties-block-ast?
(mapv remove-block-ast-pos
(mldoc/->edn content format))))))))
(defn- update-level-in-block-ast-coll
[block-ast-coll origin-level]
(mapv
(fn [block-ast]
(let [[ast-type ast-content] block-ast]
(if (= ast-type "Heading")
[ast-type (update ast-content :level #(+ (dec %) origin-level))]
block-ast)))
block-ast-coll))
(defn- plain-indent-inline-ast
[level & {:keys [spaces] :or {spaces " "}}]
["Plain" (str (reduce str (repeat (dec level) "\t")) spaces)])
(defn- mk-paragraph-ast
[inline-coll meta]
(with-meta ["Paragraph" inline-coll] meta))
;;; internal utils (ends)
;;; utils
(defn priority->string
[priority]
(str "[#" priority "]"))
(defn- repetition-to-string
[[[kind] [duration] n]]
(let [kind (case kind
"Dotted" "."
"Plus" "+"
"DoublePlus" "++")]
(str kind n (string/lower-case (str (first duration))))))
(defn timestamp-to-string
[{:keys [date time repetition wday active]}]
(let [{:keys [year month day]} date
{:keys [hour min]} time
[open close] (if active ["<" ">"] ["[" "]"])
repetition (if repetition
(str " " (repetition-to-string repetition))
"")
hour (when hour (util/zero-pad hour))
min (when min (util/zero-pad min))
time (cond
(and hour min)
(util/format " %s:%s" hour min)
hour
(util/format " %s" hour)
:else
"")]
(util/format "%s%s-%s-%s %s%s%s%s"
open
(str year)
(util/zero-pad month)
(util/zero-pad day)
wday
time
repetition
close)))
(defn hashtag-value->string
[inline-coll]
(reduce str
(mapv
(fn [inline]
(let [[ast-type ast-content] inline]
(case ast-type
"Nested_link"
(:content ast-content)
"Link"
(:full_text ast-content)
"Plain"
ast-content)))
inline-coll)))
;; Utils
(defn <get-all-pages (defn <get-all-pages
[repo] [repo]
(state/<invoke-db-worker :thread-api/export-get-all-pages repo)) (state/<invoke-db-worker :thread-api/export-get-all-pages repo))
@@ -209,625 +54,19 @@
:format :markdown}) :format :markdown})
page->content))) page->content)))
;;; utils (ends) ;; Aliased fns requiring cli-export-common dynamic bindings e.g. cli-export-common/*current-repo*
(def replace-block&page-reference&embed cli-export-common/replace-block&page-reference&embed)
(def replace-Heading-with-Paragraph cli-export-common/replace-Heading-with-Paragraph)
;;; replace block-ref, block-embed, page-embed ;; Aliased fns
(def priority->string cli-export-common/priority->string)
(defn- replace-block-reference-in-heading (def timestamp-to-string cli-export-common/timestamp-to-string)
[{:keys [title] :as ast-content}] (def hashtag-value->string cli-export-common/hashtag-value->string)
(let [inline-coll title (def remove-block-ast-pos cli-export-common/remove-block-ast-pos)
inline-coll* (def Properties-block-ast? cli-export-common/Properties-block-ast?)
(mapcatv (def keep-only-level<=n cli-export-common/keep-only-level<=n)
#(match [%] (def remove-emphasis cli-export-common/remove-emphasis)
[["Link" {:url ["Block_ref" block-uuid]}]] (def remove-page-ref-brackets cli-export-common/remove-page-ref-brackets)
(let [[[_ {title-inline-coll :title}]] (def remove-tags cli-export-common/remove-tags)
(block-uuid->ast (uuid block-uuid))] (def remove-prefix-spaces-in-Plain cli-export-common/remove-prefix-spaces-in-Plain)
(set! *state* (assoc-in *state* [:replace-ref-embed :block-ref-replaced?] true)) (def walk-block-ast cli-export-common/walk-block-ast)
title-inline-coll)
:else [%])
inline-coll)]
(assoc ast-content :title inline-coll*)))
(defn- replace-block-reference-in-paragraph
[inline-coll]
(mapcatv
#(match [%]
[["Link" {:url ["Block_ref" block-uuid]}]]
(let [[[_ {title-inline-coll :title}]]
(block-uuid->ast (uuid block-uuid))]
(set! *state* (assoc-in *state* [:replace-ref-embed :block-ref-replaced?] true))
title-inline-coll)
:else [%])
inline-coll))
(declare replace-block-references)
(defn- replace-block-reference-in-list
[list-items]
(mapv
(fn [{block-ast-coll :content sub-items :items :as item}]
(assoc item
:content (mapv replace-block-references block-ast-coll)
:items (replace-block-reference-in-list sub-items)))
list-items))
(defn- replace-block-reference-in-quote
[block-ast-coll]
(mapv replace-block-references block-ast-coll))
(defn- replace-block-reference-in-table
[{:keys [header groups] :as table}]
(let [header*
(mapv
(fn [col]
(mapcatv
#(match [%]
[["Link" {:url ["Block_ref" block-uuid]}]]
(let [[[_ {title-inline-coll :title}]]
(block-uuid->ast (uuid block-uuid))]
(set! *state* (assoc-in *state* [:replace-ref-embed :block-ref-replaced?] true))
title-inline-coll)
:else [%])
col))
header)
groups*
(mapv
(fn [group]
(mapv
(fn [row]
(mapv
(fn [col]
(mapcatv
#(match [%]
[["Link" {:url ["Block_ref" block-uuid]}]]
(let [[[_ {title-inline-coll :title}]]
(block-uuid->ast (uuid block-uuid))]
(set! *state* (assoc-in *state* [:replace-ref-embed :block-ref-replaced?] true))
title-inline-coll)
:else [%])
col))
row))
group))
groups)]
(assoc table :header header* :groups groups*)))
(defn- replace-block-references
[block-ast]
(let [[ast-type ast-content] block-ast]
(case ast-type
"Heading"
[ast-type (replace-block-reference-in-heading ast-content)]
"Paragraph"
(mk-paragraph-ast (replace-block-reference-in-paragraph ast-content) (meta block-ast))
"List"
[ast-type (replace-block-reference-in-list ast-content)]
"Quote"
[ast-type (replace-block-reference-in-quote ast-content)]
"Table"
[ast-type (replace-block-reference-in-table ast-content)]
;; else
block-ast)))
(defn- replace-block-references-until-stable
[block-ast]
(binding [*state* *state*]
(loop [block-ast block-ast]
(let [block-ast* (replace-block-references block-ast)]
(if (get-in *state* [:replace-ref-embed :block-ref-replaced?])
(do (set! *state* (assoc-in *state* [:replace-ref-embed :block-ref-replaced?] false))
(recur block-ast*))
block-ast*)))))
(defn- replace-block-embeds-helper
[current-paragraph-inlines block-uuid blocks-tcoll level]
(let [block-uuid* (subs block-uuid 2 (- (count block-uuid) 2))
ast-coll (update-level-in-block-ast-coll
(block-uuid->ast-with-children (uuid block-uuid*))
level)]
(cond-> blocks-tcoll
(seq current-paragraph-inlines)
(conj! ["Paragraph" current-paragraph-inlines])
true
(#(reduce conj! % ast-coll)))))
(defn- replace-page-embeds-helper
[current-paragraph-inlines page-name blocks-tcoll level]
(let [page-name* (subs page-name 2 (- (count page-name) 2))
ast-coll (update-level-in-block-ast-coll
(page-name->ast page-name*)
level)]
(cond-> blocks-tcoll
(seq current-paragraph-inlines)
(conj! ["Paragraph" current-paragraph-inlines])
true
(#(reduce conj! % ast-coll)))))
(defn- replace-block&page-embeds-in-heading
[{inline-coll :title origin-level :level :as ast-content}]
(set! *state* (assoc-in *state* [:replace-ref-embed :current-level] origin-level))
(if (empty? inline-coll)
;; it's just a empty Heading, return itself
[["Heading" ast-content]]
(loop [[inline & other-inlines] inline-coll
heading-exist? false
current-paragraph-inlines []
r (transient [])]
(if-not inline
(persistent!
(if (seq current-paragraph-inlines)
(conj! r (if heading-exist?
["Paragraph" current-paragraph-inlines]
["Heading" (assoc ast-content :title current-paragraph-inlines)]))
r))
(match [inline]
[["Macro" {:name "embed" :arguments [block-uuid-or-page-name]}]]
(cond
(and (string/starts-with? block-uuid-or-page-name "((")
(string/ends-with? block-uuid-or-page-name "))"))
(do (set! *state* (assoc-in *state* [:replace-ref-embed :block&page-embed-replaced?] true))
(recur other-inlines true []
(replace-block-embeds-helper
current-paragraph-inlines block-uuid-or-page-name r origin-level)))
(and (string/starts-with? block-uuid-or-page-name "[[")
(string/ends-with? block-uuid-or-page-name "]]"))
(do (set! *state* (assoc-in *state* [:replace-ref-embed :block&page-embed-replaced?] true))
(recur other-inlines true []
(replace-page-embeds-helper
current-paragraph-inlines block-uuid-or-page-name r origin-level)))
:else ;; not ((block-uuid)) or [[page-name]], just drop the original ast
(recur other-inlines heading-exist? current-paragraph-inlines r))
:else
(let [current-paragraph-inlines*
(if (and (empty? current-paragraph-inlines)
heading-exist?)
(conj current-paragraph-inlines (plain-indent-inline-ast origin-level))
current-paragraph-inlines)]
(recur other-inlines heading-exist? (conj current-paragraph-inlines* inline) r)))))))
(defn- replace-block&page-embeds-in-paragraph
[inline-coll meta]
(let [current-level (get-in *state* [:replace-ref-embed :current-level])]
(loop [[inline & other-inlines] inline-coll
current-paragraph-inlines []
just-after-embed? false
blocks (transient [])]
(if-not inline
(let [[first-block & other-blocks] (persistent!
(if (seq current-paragraph-inlines)
(conj! blocks ["Paragraph" current-paragraph-inlines])
blocks))]
(if first-block
(apply vector (with-meta first-block meta) other-blocks)
[]))
(match [inline]
[["Macro" {:name "embed" :arguments [block-uuid-or-page-name]}]]
(cond
(and (string/starts-with? block-uuid-or-page-name "((")
(string/ends-with? block-uuid-or-page-name "))"))
(do (set! *state* (assoc-in *state* [:replace-ref-embed :block&page-embed-replaced?] true))
(recur other-inlines [] true
(replace-block-embeds-helper
current-paragraph-inlines block-uuid-or-page-name blocks current-level)))
(and (string/starts-with? block-uuid-or-page-name "[[")
(string/ends-with? block-uuid-or-page-name "]]"))
(do (set! *state* (assoc-in *state* [:replace-ref-embed :block&page-embed-replaced?] true))
(recur other-inlines [] true
(replace-page-embeds-helper
current-paragraph-inlines block-uuid-or-page-name blocks current-level)))
:else ;; not ((block-uuid)) or [[page-name]], just drop the original ast
(recur other-inlines current-paragraph-inlines false blocks))
:else
(let [current-paragraph-inlines*
(if just-after-embed?
(conj current-paragraph-inlines (plain-indent-inline-ast current-level))
current-paragraph-inlines)]
(recur other-inlines (conj current-paragraph-inlines* inline) false blocks)))))))
(declare replace-block&page-embeds)
(defn- replace-block&page-embeds-in-list-helper
[list-items]
(binding [*state* (update-in *state* [:replace-ref-embed :current-level] inc)]
(mapv
(fn [{block-ast-coll :content sub-items :items :as item}]
(assoc item
:content (mapcatv replace-block&page-embeds block-ast-coll)
:items (replace-block&page-embeds-in-list-helper sub-items)))
list-items)))
(defn- replace-block&page-embeds-in-list
[list-items]
[["List" (replace-block&page-embeds-in-list-helper list-items)]])
(defn- replace-block&page-embeds-in-quote
[block-ast-coll]
(->> block-ast-coll
(mapcatv replace-block&page-embeds)
(vector "Quote")
vector))
(defn- replace-block&page-embeds
[block-ast]
(let [[ast-type ast-content] block-ast]
(case ast-type
"Heading"
(replace-block&page-embeds-in-heading ast-content)
"Paragraph"
(replace-block&page-embeds-in-paragraph ast-content (meta block-ast))
"List"
(replace-block&page-embeds-in-list ast-content)
"Quote"
(replace-block&page-embeds-in-quote ast-content)
"Table"
;; TODO: block&page embeds in table are not replaced yet
[block-ast]
;; else
[block-ast])))
(defn replace-block&page-reference&embed
"add meta :embed-depth to the embed replaced block-ast,
to avoid too deep block-ref&embed (or maybe it's a cycle)"
[block-ast-coll]
(loop [block-ast-coll block-ast-coll
result-block-ast-tcoll (transient [])
block-ast-coll-to-replace-references []
block-ast-coll-to-replace-embeds []]
(cond
(seq block-ast-coll-to-replace-references)
(let [[block-ast-to-replace-ref & other-block-asts-to-replace-ref]
block-ast-coll-to-replace-references
embed-depth (:embed-depth (meta block-ast-to-replace-ref) 0)
block-ast-replaced (-> (replace-block-references-until-stable block-ast-to-replace-ref)
(with-meta {:embed-depth embed-depth}))]
(if (>= embed-depth 5)
;; if :embed-depth >= 5, dont replace embed for this block anymore
;; there is too deep, or maybe it just a ref/embed cycle
(recur block-ast-coll (conj! result-block-ast-tcoll block-ast-replaced)
(vec other-block-asts-to-replace-ref) block-ast-coll-to-replace-embeds)
(recur block-ast-coll result-block-ast-tcoll (vec other-block-asts-to-replace-ref)
(conj block-ast-coll-to-replace-embeds block-ast-replaced))))
(seq block-ast-coll-to-replace-embeds)
(let [[block-ast-to-replace-embed & other-block-asts-to-replace-embed]
block-ast-coll-to-replace-embeds
embed-depth (:embed-depth (meta block-ast-to-replace-embed) 0)
block-ast-coll-replaced (->> (replace-block&page-embeds block-ast-to-replace-embed)
(mapv #(with-meta % {:embed-depth (inc embed-depth)})))]
(if (get-in *state* [:replace-ref-embed :block&page-embed-replaced?])
(do (set! *state* (assoc-in *state* [:replace-ref-embed :block&page-embed-replaced?] false))
(recur block-ast-coll result-block-ast-tcoll
(concatv block-ast-coll-to-replace-references block-ast-coll-replaced)
(vec other-block-asts-to-replace-embed)))
(recur block-ast-coll (reduce conj! result-block-ast-tcoll block-ast-coll-replaced)
(vec block-ast-coll-to-replace-references) (vec other-block-asts-to-replace-embed))))
:else
(let [[block-ast & other-block-ast] block-ast-coll]
(if-not block-ast
(persistent! result-block-ast-tcoll)
(recur other-block-ast result-block-ast-tcoll
(conj block-ast-coll-to-replace-references block-ast)
(vec block-ast-coll-to-replace-embeds)))))))
;;; replace block-ref, block-embed, page-embed (ends)
(def remove-block-ast-pos
"[[ast-type ast-content] _pos] -> [ast-type ast-content]"
first)
(defn Properties-block-ast?
[[tp _]]
(= tp "Properties"))
(defn replace-Heading-with-Paragraph
"works on block-ast
replace all heading with paragraph when indent-style is no-indent"
[heading-ast]
(let [[heading-type {:keys [title marker priority size]}] heading-ast]
(if (= heading-type "Heading")
(let [inline-coll
(cond->> title
priority (cons ["Plain" (str (priority->string priority) " ")])
marker (cons ["Plain" (str marker " ")])
size (cons ["Plain" (str (reduce str (repeat size "#")) " ")])
true vec)]
(mk-paragraph-ast inline-coll {:origin-ast heading-ast}))
heading-ast)))
(defn keep-only-level<=n
[block-ast-coll n]
(-> (reduce
(fn [{:keys [result-ast-tcoll accepted-heading] :as r} ast]
(let [[heading-type {level :level}] ast
is-heading? (= heading-type "Heading")]
(cond
(and (not is-heading?) accepted-heading)
{:result-ast-tcoll (conj! result-ast-tcoll ast) :accepted-heading accepted-heading}
(and (not is-heading?) (not accepted-heading))
r
(and is-heading? (<= level n))
{:result-ast-tcoll (conj! result-ast-tcoll ast) :accepted-heading true}
(and is-heading? (> level n))
{:result-ast-tcoll result-ast-tcoll :accepted-heading false})))
{:result-ast-tcoll (transient []) :accepted-heading false}
block-ast-coll)
:result-ast-tcoll
persistent!))
;;; inline transformers
(defn remove-emphasis
":mapcat-fns-on-inline-ast"
[inline-ast]
(let [[ast-type ast-content] inline-ast]
(case ast-type
"Emphasis"
(let [[_ inline-coll] ast-content]
inline-coll)
;; else
[inline-ast])))
(defn remove-page-ref-brackets
":map-fns-on-inline-ast"
[inline-ast]
(let [[ast-type ast-content] inline-ast]
(case ast-type
"Link"
(let [{:keys [url label]} ast-content]
(if (and (= "Page_ref" (first url))
(or (empty? label)
(= label [["Plain" ""]])))
["Plain" (second url)]
inline-ast))
;; else
inline-ast)))
(defn remove-tags
":mapcat-fns-on-inline-ast"
[inline-ast]
(let [[ast-type _ast-content] inline-ast]
(case ast-type
"Tag"
[]
;; else
[inline-ast])))
(defn remove-prefix-spaces-in-Plain
[inline-coll]
(:r
(reduce
(fn [{:keys [r after-break-line?]} ast]
(let [[ast-type ast-content] ast]
(case ast-type
"Plain"
(let [trimmed-content (string/triml ast-content)]
(if after-break-line?
(if (empty? trimmed-content)
{:r r :after-break-line? false}
{:r (conj r ["Plain" trimmed-content]) :after-break-line? false})
{:r (conj r ast) :after-break-line? false}))
("Break_Line" "Hard_Break_Line")
{:r (conj r ast) :after-break-line? true}
;; else
{:r (conj r ast) :after-break-line? false})))
{:r [] :after-break-line? true}
inline-coll)))
;;; inline transformers (ends)
;;; walk on block-ast, apply inline transformers
(defn- walk-block-ast-helper
[inline-coll map-fns-on-inline-ast mapcat-fns-on-inline-ast fns-on-inline-coll]
(->>
(reduce (fn [inline-coll f] (f inline-coll)) inline-coll fns-on-inline-coll)
(mapv #(reduce (fn [inline-ast f] (f inline-ast)) % map-fns-on-inline-ast))
(mapcatv #(reduce
(fn [inline-ast-coll f] (mapcatv f inline-ast-coll)) [%] mapcat-fns-on-inline-ast))))
(declare walk-block-ast)
(defn- walk-block-ast-for-list
[list-items map-fns-on-inline-ast mapcat-fns-on-inline-ast]
(mapv
(fn [{block-ast-coll :content sub-items :items :as item}]
(assoc item
:content
(mapv
(partial walk-block-ast
{:map-fns-on-inline-ast map-fns-on-inline-ast
:mapcat-fns-on-inline-ast mapcat-fns-on-inline-ast})
block-ast-coll)
:items
(walk-block-ast-for-list sub-items map-fns-on-inline-ast mapcat-fns-on-inline-ast)))
list-items))
(defn walk-block-ast
[{:keys [map-fns-on-inline-ast mapcat-fns-on-inline-ast fns-on-inline-coll] :as fns}
block-ast]
(let [[ast-type ast-content] block-ast]
(case ast-type
"Paragraph"
(mk-paragraph-ast
(walk-block-ast-helper ast-content map-fns-on-inline-ast mapcat-fns-on-inline-ast fns-on-inline-coll)
(meta block-ast))
"Heading"
(let [{:keys [title]} ast-content]
["Heading"
(assoc ast-content
:title
(walk-block-ast-helper title map-fns-on-inline-ast mapcat-fns-on-inline-ast fns-on-inline-coll))])
"List"
["List" (walk-block-ast-for-list ast-content map-fns-on-inline-ast mapcat-fns-on-inline-ast)]
"Quote"
["Quote" (mapv (partial walk-block-ast fns) ast-content)]
"Footnote_Definition"
(let [[name contents] (rest block-ast)]
["Footnote_Definition"
name (walk-block-ast-helper contents map-fns-on-inline-ast mapcat-fns-on-inline-ast fns-on-inline-coll)])
"Table"
(let [{:keys [header groups]} ast-content
header* (mapv
#(walk-block-ast-helper % map-fns-on-inline-ast mapcat-fns-on-inline-ast fns-on-inline-coll)
header)
groups* (mapv
(fn [group]
(mapv
(fn [row]
(mapv
(fn [col]
(walk-block-ast-helper col map-fns-on-inline-ast mapcat-fns-on-inline-ast fns-on-inline-coll))
row))
group))
groups)]
["Table" (assoc ast-content :header header* :groups groups*)])
;; else
block-ast)))
;;; walk on block-ast, apply inline transformers (ends)
;;; simple ast
(def simple-ast-malli-schema
(mu/closed-schema
[:or
[:map
[:type [:= :raw-text]]
[:content :string]]
[:map
[:type [:= :space]]]
[:map
[:type [:= :newline]]
[:line-count :int]]
[:map
[:type [:= :indent]]
[:level :int]
[:extra-space-count :int]]]))
(defn raw-text [& contents]
{:type :raw-text :content (reduce str contents)})
(def space {:type :space})
(defn newline* [line-count]
{:type :newline :line-count line-count})
(defn indent [level extra-space-count]
{:type :indent :level level :extra-space-count extra-space-count})
(defn- simple-ast->string
[simple-ast]
{:pre [(m/validate simple-ast-malli-schema simple-ast)]}
(case (:type simple-ast)
:raw-text (:content simple-ast)
:space " "
:newline (reduce str (repeat (:line-count simple-ast) "\n"))
:indent (reduce str (concatv (repeat (:level simple-ast) "\t")
(repeat (:extra-space-count simple-ast) " ")))))
(defn- merge-adjacent-spaces&newlines
[simple-ast-coll]
(loop [r (transient [])
last-ast nil
last-raw-text-space-suffix? false
last-raw-text-newline-suffix? false
[simple-ast & other-ast-coll] simple-ast-coll]
(if (nil? simple-ast)
(persistent! (if last-ast (conj! r last-ast) r))
(let [tp (:type simple-ast)
last-ast-type (:type last-ast)]
(case tp
:space
(if (or (contains? #{:space :newline :indent} last-ast-type)
last-raw-text-space-suffix?
last-raw-text-newline-suffix?)
;; drop this :space
(recur r last-ast last-raw-text-space-suffix? last-raw-text-newline-suffix? other-ast-coll)
(recur (if last-ast (conj! r last-ast) r) simple-ast false false other-ast-coll))
:newline
(case last-ast-type
(:space :indent) ;; drop last-ast
(recur r simple-ast false false other-ast-coll)
:newline
(let [last-newline-count (:line-count last-ast)
current-newline-count (:line-count simple-ast)
kept-ast (if (> last-newline-count current-newline-count) last-ast simple-ast)]
(recur r kept-ast false false other-ast-coll))
:raw-text
(if last-raw-text-newline-suffix?
(recur r last-ast last-raw-text-space-suffix? last-raw-text-newline-suffix? other-ast-coll)
(recur (if last-ast (conj! r last-ast) r) simple-ast false false other-ast-coll))
;; no-last-ast
(recur r simple-ast false false other-ast-coll))
:indent
(case last-ast-type
(:space :indent) ; drop last-ast
(recur r simple-ast false false other-ast-coll)
:newline
(recur (if last-ast (conj! r last-ast) r) simple-ast false false other-ast-coll)
:raw-text
(if last-raw-text-space-suffix?
;; drop this :indent
(recur r last-ast last-raw-text-space-suffix? last-raw-text-newline-suffix? other-ast-coll)
(recur (if last-ast (conj! r last-ast) r) simple-ast false false other-ast-coll))
;; no-last-ast
(recur r simple-ast false false other-ast-coll))
:raw-text
(let [content (:content simple-ast)
empty-content? (empty? content)
first-ch (first content)
last-ch (let [num (count content)]
(when (pos? num)
(nth content (dec num))))
newline-prefix? (some-> first-ch #{"\r" "\n"} boolean)
newline-suffix? (some-> last-ch #{"\n"} boolean)
space-prefix? (some-> first-ch #{" "} boolean)
space-suffix? (some-> last-ch #{" "} boolean)]
(cond
empty-content? ;drop this raw-text
(recur r last-ast last-raw-text-space-suffix? last-raw-text-newline-suffix? other-ast-coll)
newline-prefix?
(case last-ast-type
(:space :indent :newline) ;drop last-ast
(recur r simple-ast space-suffix? newline-suffix? other-ast-coll)
:raw-text
(recur (if last-ast (conj! r last-ast) r) simple-ast space-suffix? newline-suffix? other-ast-coll)
;; no-last-ast
(recur r simple-ast space-suffix? newline-suffix? other-ast-coll))
space-prefix?
(case last-ast-type
(:space :indent) ;drop last-ast
(recur r simple-ast space-suffix? newline-suffix? other-ast-coll)
(:newline :raw-text)
(recur (if last-ast (conj! r last-ast) r) simple-ast space-suffix? newline-suffix? other-ast-coll)
;; no-last-ast
(recur r simple-ast space-suffix? newline-suffix? other-ast-coll))
:else
(recur (if last-ast (conj! r last-ast) r) simple-ast space-suffix? newline-suffix? other-ast-coll))))))))
(defn simple-asts->string
[simple-ast-coll]
(->> simple-ast-coll
merge-adjacent-spaces&newlines
merge-adjacent-spaces&newlines
(mapv simple-ast->string)
string/join))
;;; simple ast (ends)
;;; TODO: walk the hiccup tree,
;;; and call escape-html on all its contents
;;;
;;; walk the hiccup tree,
;;; and call escape-html on all its contents (ends)

View File

@@ -6,12 +6,15 @@
[clojure.zip :as z] [clojure.zip :as z]
[frontend.db :as db] [frontend.db :as db]
[frontend.format.mldoc :as mldoc] [frontend.format.mldoc :as mldoc]
[frontend.handler.export.common :as common :refer [*state*]] [frontend.handler.export.common :as common]
[frontend.handler.export.zip-helper :refer [get-level goto-last [frontend.handler.export.zip-helper :refer [get-level goto-last
goto-level]] goto-level]]
[frontend.util :as util :refer [concatv mapcatv removev]] [frontend.util :as util]
[hiccups.runtime :as h] [hiccups.runtime :as h]
[malli.core :as m])) [logseq.cli.common.export.common :as cli-export-common :refer [*state*]]
[logseq.cli.common.util :refer-macros [concatv mapcatv removev]]
[malli.core :as m]
[frontend.db.conn :as conn]))
(def ^:private hiccup-malli-schema (def ^:private hiccup-malli-schema
[:cat :keyword [:* :any]]) [:cat :keyword [:* :any]])
@@ -426,6 +429,9 @@
first-block (and (coll? root-block-uuids-or-page-uuid) first-block (and (coll? root-block-uuids-or-page-uuid)
(db/entity [:block/uuid (first root-block-uuids-or-page-uuid)])) (db/entity [:block/uuid (first root-block-uuids-or-page-uuid)]))
format (get first-block :block/format :markdown)] format (get first-block :block/format :markdown)]
(export-helper content format options))) (binding [cli-export-common/*current-repo* repo
cli-export-common/*current-db* (conn/get-db repo)
cli-export-common/*content-config* (common/get-content-config)]
(export-helper content format options))))
;;; export fns (ends) ;;; export fns (ends)

View File

@@ -8,15 +8,18 @@
[frontend.db :as db] [frontend.db :as db]
[frontend.extensions.zip :as zip] [frontend.extensions.zip :as zip]
[frontend.format.mldoc :as mldoc] [frontend.format.mldoc :as mldoc]
[frontend.handler.export.common :as common :refer [frontend.handler.export.common :as common]
[*state* raw-text simple-asts->string space]]
[frontend.handler.export.zip-helper :refer [get-level goto-last [frontend.handler.export.zip-helper :refer [get-level goto-last
goto-level]] goto-level]]
[frontend.util :as util :refer [concatv mapcatv removev]] [frontend.util :as util]
[goog.dom :as gdom] [goog.dom :as gdom]
[hiccups.runtime :as h] [hiccups.runtime :as h]
[logseq.cli.common.export.common :as cli-export-common :refer
[*state* raw-text simple-asts->string space]]
[logseq.cli.common.util :refer-macros [concatv mapcatv removev]]
[logseq.common.path :as path] [logseq.common.path :as path]
[promesa.core :as p])) [promesa.core :as p]
[frontend.db.conn :as conn]))
;;; *opml-state* ;;; *opml-state*
(def ^:private ^:dynamic (def ^:private ^:dynamic
@@ -435,29 +438,35 @@
{:pre [(or (coll? root-block-uuids-or-page-uuid) {:pre [(or (coll? root-block-uuids-or-page-uuid)
(uuid? root-block-uuids-or-page-uuid))]} (uuid? root-block-uuids-or-page-uuid))]}
(util/profile (util/profile
:export-blocks-as-opml :export-blocks-as-opml
(let [content (let [content
(if (uuid? root-block-uuids-or-page-uuid) (if (uuid? root-block-uuids-or-page-uuid)
;; page ;; page
(common/get-page-content root-block-uuids-or-page-uuid) (common/get-page-content root-block-uuids-or-page-uuid)
(common/root-block-uuids->content repo root-block-uuids-or-page-uuid)) (common/root-block-uuids->content repo root-block-uuids-or-page-uuid))
title (if (uuid? root-block-uuids-or-page-uuid) title (if (uuid? root-block-uuids-or-page-uuid)
(:block/title (db/entity [:block/uuid root-block-uuids-or-page-uuid])) (:block/title (db/entity [:block/uuid root-block-uuids-or-page-uuid]))
"untitled") "untitled")
first-block (and (coll? root-block-uuids-or-page-uuid) first-block (and (coll? root-block-uuids-or-page-uuid)
(db/entity [:block/uuid (first root-block-uuids-or-page-uuid)])) (db/entity [:block/uuid (first root-block-uuids-or-page-uuid)]))
format (get first-block :block/format :markdown)] format (get first-block :block/format :markdown)]
(export-helper content format options :title title)))) (binding [cli-export-common/*current-repo* repo
cli-export-common/*current-db* (conn/get-db repo)
cli-export-common/*content-config* (common/get-content-config)]
(export-helper content format options :title title)))))
(defn- export-files-as-opml (defn- export-files-as-opml
"options see also `export-blocks-as-opml`" "options see also `export-blocks-as-opml`"
[files options] [repo files options]
(mapv (binding [cli-export-common/*current-repo* repo
(fn [{:keys [path content title format]}] cli-export-common/*current-db* (conn/get-db repo)
(when (and title (not (string/blank? content))) cli-export-common/*content-config* (common/get-content-config)]
(util/profile (print-str :export-files-as-opml path) (mapv
[path (export-helper content format options :title title)]))) (fn [{:keys [path content title format]}]
files)) (when (and title (not (string/blank? content)))
(util/profile (print-str :export-files-as-opml path)
[path (export-helper content format options :title title)])))
files)))
(defn export-repo-as-opml! (defn export-repo-as-opml!
[repo] [repo]
@@ -466,7 +475,7 @@
(let [repo' (if (config/db-based-graph? repo) (let [repo' (if (config/db-based-graph? repo)
(string/replace repo config/db-version-prefix "") (string/replace repo config/db-version-prefix "")
(path/basename repo)) (path/basename repo))
files (->> (export-files-as-opml files nil) files (->> (export-files-as-opml repo files nil)
(clojure.core/remove nil?)) (clojure.core/remove nil?))
zip-file-name (str repo' "_opml_" (quot (util/time-ms) 1000))] zip-file-name (str repo' "_opml_" (quot (util/time-ms) 1000))]
(p/let [zipfile (zip/make-zip zip-file-name files repo')] (p/let [zipfile (zip/make-zip zip-file-name files repo')]

View File

@@ -3,506 +3,20 @@
(:require [clojure.string :as string] (:require [clojure.string :as string]
[frontend.config :as config] [frontend.config :as config]
[frontend.db :as db] [frontend.db :as db]
[frontend.db.conn :as conn]
[frontend.extensions.zip :as zip] [frontend.extensions.zip :as zip]
[frontend.format.mldoc :as mldoc] [frontend.handler.export.common :as common]
[frontend.handler.export.common :as common :refer [frontend.state :as state]
[*state* indent newline* raw-text simple-ast-malli-schema [frontend.util :as util]
simple-asts->string space]]
[frontend.util :as util :refer [concatv mapcatv removev]]
[goog.dom :as gdom] [goog.dom :as gdom]
[logseq.common.path :as path] [logseq.common.path :as path]
[logseq.cli.common.export.common :as cli-export-common]
[logseq.cli.common.export.text :as cli-export-text]
[logseq.db :as ldb] [logseq.db :as ldb]
[logseq.graph-parser.schema.mldoc :as mldoc-schema]
[malli.core :as m]
[promesa.core :as p])) [promesa.core :as p]))
;;; block-ast, inline-ast -> simple-ast
(defn indent-with-2-spaces
"also consider (get-in *state* [:export-options :indent-style])"
[level]
(let [indent-style (get-in *state* [:export-options :indent-style])]
(case indent-style
"dashes" (indent level 2)
("spaces" "no-indent") (indent level 0)
(assert false (print-str "unknown indent-style:" indent-style)))))
(declare inline-ast->simple-ast
block-ast->simple-ast)
(defn- block-heading
[{:keys [title _tags marker level _numbering priority _anchor _meta _unordered size]}]
(let [indent-style (get-in *state* [:export-options :indent-style])
priority* (and priority (raw-text (common/priority->string priority)))
heading* (if (= indent-style "dashes")
[(indent (dec level) 0) (raw-text "-")]
[(indent (dec level) 0)])
size* (and size [space (raw-text (reduce str (repeat size "#")))])
marker* (and marker (raw-text marker))]
(set! *state* (assoc *state* :current-level level))
(let [simple-asts
(removev nil? (concatv
(when (and (get-in *state* [:export-options :newline-after-block])
(not (get-in *state* [:newline-after-block :current-block-is-first-heading-block?])))
[(newline* 2)])
heading* size*
[space marker* space priority* space]
(mapcatv inline-ast->simple-ast title)
[(newline* 1)]))]
(set! *state* (assoc-in *state* [:newline-after-block :current-block-is-first-heading-block?] false))
simple-asts)))
(declare block-list)
(defn- block-list-item
[{:keys [content items number _name checkbox]}]
(let [content* (mapcatv block-ast->simple-ast content)
number* (raw-text
(if number
(str number ". ")
"* "))
checkbox* (raw-text
(if (some? checkbox)
(if (boolean checkbox)
"[X]" "[ ]")
""))
current-level (get *state* :current-level 1)
indent' (when (> current-level 1)
(indent (dec current-level) 0))
items* (block-list items :in-list? true)]
(concatv [indent' number* checkbox* space]
content*
[(newline* 1)]
items*
[(newline* 1)])))
(defn- block-list
[l & {:keys [in-list?]}]
(binding [*state* (update *state* :current-level inc)]
(concatv (mapcatv block-list-item l)
(when (and (pos? (count l))
(not in-list?))
[(newline* 2)]))))
(defn- block-property-drawer
[properties]
(when-not (get-in *state* [:export-options :remove-properties?])
(let [level (dec (get *state* :current-level 1))
indent' (indent-with-2-spaces level)]
(reduce
(fn [r [k v]]
(conj r indent' (raw-text k "::") space (raw-text v) (newline* 1)))
[] properties))))
(defn- block-example
[l]
(let [level (dec (get *state* :current-level 1))]
(mapcatv
(fn [line]
[(indent-with-2-spaces level)
(raw-text " ")
(raw-text line)
(newline* 1)])
l)))
(defn- remove-max-prefix-spaces
[lines]
(let [common-prefix-spaces
(reduce
(fn [r line]
(if (string/blank? line)
r
(let [leading-spaces (re-find #"^\s+" line)]
(if (nil? r)
leading-spaces
(if (string/starts-with? r leading-spaces)
leading-spaces
r)))))
nil
lines)
pattern (re-pattern (str "^" common-prefix-spaces))]
(mapv (fn [line] (string/replace-first line pattern "")) lines)))
(defn- block-src
[{:keys [lines language]}]
(let [level (dec (get *state* :current-level 1))
lines* (if (= "no-indent" (get-in *state* [:export-options :indent-style]))
(remove-max-prefix-spaces lines)
lines)]
(concatv
[(indent-with-2-spaces level) (raw-text "```")]
(when language [(raw-text language)])
[(newline* 1)]
(mapv raw-text lines*)
[(indent-with-2-spaces level) (raw-text "```") (newline* 1)])))
(defn- block-quote
[block-coll]
(let [level (dec (get *state* :current-level 1))]
(binding [*state* (assoc *state* :indent-after-break-line? true)]
(concatv (mapcatv (fn [block]
(let [block-simple-ast (block-ast->simple-ast block)]
(when (seq block-simple-ast)
(concatv [(indent-with-2-spaces level) (raw-text ">") space]
block-simple-ast))))
block-coll)
[(newline* 2)]))))
(declare inline-latex-fragment)
(defn- block-latex-fragment
[ast-content]
(inline-latex-fragment ast-content))
(defn- block-latex-env
[[name options content]]
(let [level (dec (get *state* :current-level 1))]
[(indent-with-2-spaces level) (raw-text "\\begin{" name "}" options)
(newline* 1)
(indent-with-2-spaces level) (raw-text content)
(newline* 1)
(indent-with-2-spaces level) (raw-text "\\end{" name "}")
(newline* 1)]))
(defn- block-displayed-math
[ast-content]
[space (raw-text "$$" ast-content "$$") space])
(defn- block-drawer
[[name lines]]
(let [level (dec (get *state* :current-level))]
(concatv
[(raw-text ":" name ":")
(newline* 1)]
(mapcatv (fn [line] [(indent-with-2-spaces level) (raw-text line)]) lines)
[(newline* 1) (raw-text ":END:") (newline* 1)])))
(defn- block-footnote-definition
[[name content]]
(concatv
[(raw-text "[^" name "]:") space]
(mapcatv inline-ast->simple-ast content)
[(newline* 1)]))
(def ^:private block-horizontal-rule [(newline* 1) (raw-text "---") (newline* 1)])
(defn- block-table
[{:keys [header groups]}]
(let [level (dec (get *state* :current-level 1))
sep-line (raw-text "|" (string/join "|" (repeat (count header) "---")) "|")
header-line
(concatv (mapcatv
(fn [h] (concatv [space (raw-text "|") space] (mapcatv inline-ast->simple-ast h)))
header)
[space (raw-text "|")])
group-lines
(mapcatv
(fn [group]
(mapcatv
(fn [row]
(concatv [(indent-with-2-spaces level)]
(mapcatv
(fn [col]
(concatv [(raw-text "|") space]
(mapcatv inline-ast->simple-ast col)
[space]))
row)
[(raw-text "|") (newline* 1)]))
group))
groups)]
(concatv [(newline* 1) (indent-with-2-spaces level)]
(when (seq header) header-line)
(when (seq header) [(newline* 1) (indent-with-2-spaces level) sep-line (newline* 1)])
group-lines)))
(defn- block-comment
[s]
(let [level (dec (get *state* :current-level 1))]
[(indent-with-2-spaces level) (raw-text "<!---") (newline* 1)
(indent-with-2-spaces level) (raw-text s) (newline* 1)
(indent-with-2-spaces level) (raw-text "-->") (newline* 1)]))
(defn- block-raw-html
[s]
(let [level (dec (get *state* :current-level 1))]
[(indent-with-2-spaces level) (raw-text s) (newline* 1)]))
(defn- block-hiccup
[s]
(let [level (dec (get *state* :current-level 1))]
[(indent-with-2-spaces level) (raw-text s) space]))
(defn- inline-link
[{full-text :full_text}]
[(raw-text full-text)])
(defn- inline-nested-link
[{content :content}]
[(raw-text content)])
(defn- inline-subscript
[inline-coll]
(concatv [(raw-text "_{")]
(mapcatv (fn [inline] (cons space (inline-ast->simple-ast inline))) inline-coll)
[(raw-text "}")]))
(defn- inline-superscript
[inline-coll]
(concatv [(raw-text "^{")]
(mapcatv (fn [inline] (cons space (inline-ast->simple-ast inline))) inline-coll)
[(raw-text "}")]))
(defn- inline-footnote-reference
[{name :name}]
[(raw-text "[" name "]")])
(defn- inline-cookie
[ast-content]
[(raw-text
(case (first ast-content)
"Absolute"
(let [[_ current total] ast-content]
(str "[" current "/" total "]"))
"Percent"
(str "[" (second ast-content) "%]")))])
(defn- inline-latex-fragment
[ast-content]
(let [[type content] ast-content
wrapper (case type
"Inline" "$"
"Displayed" "$$")]
[space (raw-text (str wrapper content wrapper)) space]))
(defn- inline-macro
[{:keys [name arguments]}]
(->
(if (= name "cloze")
(string/join "," arguments)
(let [l (cond-> ["{{" name]
(pos? (count arguments)) (conj "(" (string/join "," arguments) ")")
true (conj "}}"))]
(string/join l)))
raw-text
vector))
(defn- inline-entity
[{unicode :unicode}]
[(raw-text unicode)])
(defn- inline-timestamp
[ast-content]
(let [[type timestamp-content] ast-content]
(-> (case type
"Scheduled" ["SCHEDULED: " (common/timestamp-to-string timestamp-content)]
"Deadline" ["DEADLINE: " (common/timestamp-to-string timestamp-content)]
"Date" [(common/timestamp-to-string timestamp-content)]
"Closed" ["CLOSED: " (common/timestamp-to-string timestamp-content)]
"Clock" ["CLOCK: " (common/timestamp-to-string (second timestamp-content))]
"Range" (let [{:keys [start stop]} timestamp-content]
[(str (common/timestamp-to-string start) "--" (common/timestamp-to-string stop))]))
string/join
raw-text
vector)))
(defn- inline-email
[{:keys [local_part domain]}]
[(raw-text (str "<" local_part "@" domain ">"))])
(defn- emphasis-wrap-with
[inline-coll em-symbol]
(binding [*state* (assoc *state* :outside-em-symbol (first em-symbol))]
(concatv [(raw-text em-symbol)]
(mapcatv inline-ast->simple-ast inline-coll)
[(raw-text em-symbol)])))
(defn- inline-emphasis
[emphasis]
(let [[[type] inline-coll] emphasis
outside-em-symbol (:outside-em-symbol *state*)]
(case type
"Bold"
(emphasis-wrap-with inline-coll (if (= outside-em-symbol "*") "__" "**"))
"Italic"
(emphasis-wrap-with inline-coll (if (= outside-em-symbol "*") "_" "*"))
"Underline"
(binding [*state* (assoc *state* :outside-em-symbol outside-em-symbol)]
(mapcatv (fn [inline] (cons space (inline-ast->simple-ast inline))) inline-coll))
"Strike_through"
(emphasis-wrap-with inline-coll "~~")
"Highlight"
(emphasis-wrap-with inline-coll "^^")
;; else
(assert false (print-str :inline-emphasis emphasis "is invalid")))))
(defn- inline-break-line
[]
[(if (= "no-indent" (get-in *state* [:export-options :indent-style]))
(raw-text "\n")
(raw-text " \n"))
(when (:indent-after-break-line? *state*)
(let [current-level (get *state* :current-level 1)]
(when (> current-level 1)
(indent-with-2-spaces (dec current-level)))))])
;; {:malli/schema ...} only works on public vars, so use m/=> here
(m/=> block-ast->simple-ast [:=> [:cat mldoc-schema/block-ast-schema] [:sequential simple-ast-malli-schema]])
(defn- block-ast->simple-ast
[block]
(let [newline-after-block? (get-in *state* [:export-options :newline-after-block])]
(removev
nil?
(let [[ast-type ast-content] block]
(case ast-type
"Paragraph"
(let [{:keys [origin-ast]} (meta block)
current-block-is-first-heading-block? (get-in *state* [:newline-after-block :current-block-is-first-heading-block?])]
(set! *state* (assoc-in *state* [:newline-after-block :current-block-is-first-heading-block?] false))
(concatv
(when (and origin-ast newline-after-block? (not current-block-is-first-heading-block?))
[(newline* 2)])
(mapcatv inline-ast->simple-ast ast-content)
(let [last-element (last ast-content)
[last-element-type] last-element]
(when (and newline-after-block? (= "Break_Line" last-element-type))
(inline-break-line)))
[(newline* 1)]))
"Paragraph_line"
(assert false "Paragraph_line is mldoc internal ast")
"Paragraph_Sep"
[(newline* ast-content)]
"Heading"
(block-heading ast-content)
"List"
(block-list ast-content)
("Directive" "Results" "Export" "CommentBlock" "Custom")
nil
"Example"
(block-example ast-content)
"Src"
(block-src ast-content)
"Quote"
(block-quote ast-content)
"Latex_Fragment"
(block-latex-fragment ast-content)
"Latex_Environment"
(block-latex-env (rest block))
"Displayed_Math"
(block-displayed-math ast-content)
"Drawer"
(block-drawer (rest block))
"Property_Drawer"
(block-property-drawer ast-content)
"Footnote_Definition"
(block-footnote-definition (rest block))
"Horizontal_Rule"
block-horizontal-rule
"Table"
(block-table ast-content)
"Comment"
(block-comment ast-content)
"Raw_Html"
(block-raw-html ast-content)
"Hiccup"
(block-hiccup ast-content)
(assert false (print-str :block-ast->simple-ast ast-type "not implemented yet")))))))
(defn- inline-ast->simple-ast
[inline]
(let [[ast-type ast-content] inline]
(case ast-type
"Emphasis"
(inline-emphasis ast-content)
("Break_Line" "Hard_Break_Line")
(inline-break-line)
"Verbatim"
[(raw-text ast-content)]
"Code"
[(raw-text "`" ast-content "`")]
"Tag"
[(raw-text (str "#" (common/hashtag-value->string ast-content)))]
"Spaces" ; what's this ast-type for ?
nil
"Plain"
[(raw-text ast-content)]
"Link"
(inline-link ast-content)
"Nested_link"
(inline-nested-link ast-content)
"Target"
[(raw-text (str "<<" ast-content ">>"))]
"Subscript"
(inline-subscript ast-content)
"Superscript"
(inline-superscript ast-content)
"Footnote_Reference"
(inline-footnote-reference ast-content)
"Cookie"
(inline-cookie ast-content)
"Latex_Fragment"
(inline-latex-fragment ast-content)
"Macro"
(inline-macro ast-content)
"Entity"
(inline-entity ast-content)
"Timestamp"
(inline-timestamp ast-content)
"Radio_Target"
[(raw-text (str "<<<" ast-content ">>>"))]
"Email"
(inline-email ast-content)
"Inline_Hiccup"
[(raw-text ast-content)]
"Inline_Html"
[(raw-text ast-content)]
("Export_Snippet" "Inline_Source_Block")
nil
(assert false (print-str :inline-ast->simple-ast ast-type "not implemented yet")))))
;;; block-ast, inline-ast -> simple-ast (ends)
;;; export fns ;;; export fns
(defn- export-helper
[content format options]
(let [remove-options (set (:remove-options options))
other-options (:other-options options)]
(binding [*state* (merge *state*
{:export-options
{:indent-style (or (:indent-style options) "dashes")
:remove-emphasis? (contains? remove-options :emphasis)
:remove-page-ref-brackets? (contains? remove-options :page-ref)
:remove-tags? (contains? remove-options :tag)
:remove-properties? (contains? remove-options :property)
:keep-only-level<=N (:keep-only-level<=N other-options)
:newline-after-block (:newline-after-block other-options)}})]
(let [ast (mldoc/->edn content format)
ast (mapv common/remove-block-ast-pos ast)
ast (removev common/Properties-block-ast? ast)
ast* (common/replace-block&page-reference&embed ast)
keep-level<=n (get-in *state* [:export-options :keep-only-level<=N])
ast* (if (pos? keep-level<=n)
(common/keep-only-level<=n ast* keep-level<=n)
ast*)
ast** (if (= "no-indent" (get-in *state* [:export-options :indent-style]))
(mapv common/replace-Heading-with-Paragraph ast*)
ast*)
config-for-walk-block-ast (cond-> {}
(get-in *state* [:export-options :remove-emphasis?])
(update :mapcat-fns-on-inline-ast conj common/remove-emphasis)
(get-in *state* [:export-options :remove-page-ref-brackets?])
(update :map-fns-on-inline-ast conj common/remove-page-ref-brackets)
(get-in *state* [:export-options :remove-tags?])
(update :mapcat-fns-on-inline-ast conj common/remove-tags)
(= "no-indent" (get-in *state* [:export-options :indent-style]))
(update :fns-on-inline-coll conj common/remove-prefix-spaces-in-Plain))
ast*** (if-not (empty? config-for-walk-block-ast)
(mapv (partial common/walk-block-ast config-for-walk-block-ast) ast**)
ast**)
simple-asts (mapcatv block-ast->simple-ast ast***)]
(simple-asts->string simple-asts)))))
(defn export-blocks-as-markdown (defn export-blocks-as-markdown
"options: "options:
:indent-style \"dashes\" | \"spaces\" | \"no-indent\" :indent-style \"dashes\" | \"spaces\" | \"no-indent\"
@@ -512,34 +26,44 @@
{:pre [(or (coll? root-block-uuids-or-page-uuid) {:pre [(or (coll? root-block-uuids-or-page-uuid)
(uuid? root-block-uuids-or-page-uuid))]} (uuid? root-block-uuids-or-page-uuid))]}
(util/profile (util/profile
:export-blocks-as-markdown :export-blocks-as-markdown
(try (try
(let [content (let [content
(cond (cond
;; page ;; page
(and (= 1 (count root-block-uuids-or-page-uuid)) (and (= 1 (count root-block-uuids-or-page-uuid))
(ldb/page? (db/entity [:block/uuid (first root-block-uuids-or-page-uuid)]))) (ldb/page? (db/entity [:block/uuid (first root-block-uuids-or-page-uuid)])))
(common/get-page-content (first root-block-uuids-or-page-uuid)) (common/get-page-content (first root-block-uuids-or-page-uuid))
(and (coll? root-block-uuids-or-page-uuid) (every? #(ldb/page? (db/entity [:block/uuid %])) root-block-uuids-or-page-uuid)) (and (coll? root-block-uuids-or-page-uuid) (every? #(ldb/page? (db/entity [:block/uuid %])) root-block-uuids-or-page-uuid))
(->> (mapv (fn [id] (:block/title (db/entity [:block/uuid id]))) root-block-uuids-or-page-uuid) (->> (mapv (fn [id] (:block/title (db/entity [:block/uuid id]))) root-block-uuids-or-page-uuid)
(string/join "\n")) (string/join "\n"))
:else :else
(common/root-block-uuids->content repo root-block-uuids-or-page-uuid)) (common/root-block-uuids->content repo root-block-uuids-or-page-uuid))
first-block (and (coll? root-block-uuids-or-page-uuid) first-block (and (coll? root-block-uuids-or-page-uuid)
(db/entity [:block/uuid (first root-block-uuids-or-page-uuid)])) (db/entity [:block/uuid (first root-block-uuids-or-page-uuid)]))
format (get first-block :block/format :markdown)] format (get first-block :block/format :markdown)]
(export-helper content format options)) (binding [cli-export-common/*current-db* (conn/get-db repo)
(catch :default e cli-export-common/*current-repo* repo
(js/console.error e))))) cli-export-common/*content-config* (common/get-content-config)]
(cli-export-text/export-helper repo content format options)))
(catch :default e
(js/console.error e)))))
(defn export-files-as-markdown (defn export-files-as-markdown
"options see also `export-blocks-as-markdown`" "options see also `export-blocks-as-markdown`"
[files options] [files options]
(mapv (let [repo (state/get-current-repo)
(fn [{:keys [path title content]}] db (conn/get-db repo)
(util/profile (print-str :export-files-as-markdown title) content-config (common/get-content-config)]
[(or path title) (export-helper content :markdown options)])) (mapv
files)) (fn [{:keys [path title content]}]
(util/profile (print-str :export-files-as-markdown title)
[(or path title)
(binding [cli-export-common/*current-db* db
cli-export-common/*current-repo* repo
cli-export-common/*content-config* content-config]
(cli-export-text/export-helper repo content :markdown options))]))
files)))
(defn export-repo-as-markdown! (defn export-repo-as-markdown!
"TODO: indent-style and remove-options" "TODO: indent-style and remove-options"

View File

@@ -1,14 +0,0 @@
(ns frontend.modules.file.core
"Convert block trees to content"
(:require [frontend.common.file.core :as common-file]
[frontend.state :as state]
[frontend.db :as db]))
;; TODO: remove this file and move export related code to worker
(defn tree->file-content
[tree opts]
(when-let [repo (state/get-current-repo)]
(let [db (db/get-db repo)
context {:export-bullet-indentation (state/get-export-bullet-indentation)}]
(common-file/tree->file-content repo db tree opts context))))

View File

@@ -1448,21 +1448,6 @@ Arg *stop: atom, reset to true to stop the loop"
(async/<! (async/timeout 5000)) (async/<! (async/timeout 5000))
(recur)))))))) (recur))))))))
(defmacro concatv
"Vector version of concat. non-lazy"
[& args]
`(vec (concat ~@args)))
(defmacro mapcatv
"Vector version of mapcat. non-lazy"
[f coll & colls]
`(vec (mapcat ~f ~coll ~@colls)))
(defmacro removev
"Vector version of remove. non-lazy"
[pred coll]
`(vec (remove ~pred ~coll)))
;; from rum ;; from rum
#?(:cljs #?(:cljs
(def schedule (def schedule

View File

@@ -1,9 +1,8 @@
(ns frontend.worker.export (ns frontend.worker.export
"Export data" "Export data"
(:require [datascript.core :as d] (:require [datascript.core :as d]
[frontend.common.file.core :as common-file] [logseq.cli.common.file :as common-file]
[logseq.db :as ldb] [logseq.db :as ldb]
[logseq.db.sqlite.create-graph :as sqlite-create-graph]
[logseq.graph-parser.property :as gp-property] [logseq.graph-parser.property :as gp-property]
[logseq.outliner.tree :as otree])) [logseq.outliner.tree :as otree]))
@@ -43,19 +42,7 @@
page' (safe-keywordize page)] page' (safe-keywordize page)]
(assoc page' :block/children children)))))) (assoc page' :block/children children))))))
(defn get-all-page->content (def get-all-page->content common-file/get-all-page->content)
[repo db options]
(let [filter-fn (if (ldb/db-based-graph? db)
(fn [ent]
(or (not (:logseq.property/built-in? ent))
(contains? sqlite-create-graph/built-in-pages-names (:block/title ent))))
(constantly true))]
(->> (d/datoms db :avet :block/name)
(map #(d/entity db (:e %)))
(filter filter-fn)
(map (fn [e]
[(:block/title e)
(common-file/block->content repo db (:block/uuid e) {} options)])))))
(defn get-debug-datoms (defn get-debug-datoms
[conn] [conn]

View File

@@ -7,7 +7,7 @@
[clojure.string :as string] [clojure.string :as string]
[datascript.core :as d] [datascript.core :as d]
[frontend.common.async-util :as async-util] [frontend.common.async-util :as async-util]
[frontend.common.file.core :as common-file] [logseq.cli.common.file :as common-file]
[frontend.common.file.util :as wfu] [frontend.common.file.util :as wfu]
[frontend.worker-common.util :as worker-util] [frontend.worker-common.util :as worker-util]
[frontend.worker.state :as worker-state] [frontend.worker.state :as worker-state]