浏览代码

enhance: working spike for export md command

export namespaces aren't reused yet. Fixed bug w/ get-block-and-children
not invoking correctly in nbb. Bumped all deps to use latest nbb-logseq
Gabriel Horner 2 月之前
父节点
当前提交
6a84e56984

+ 1 - 0
deps/cli/.carve/ignore

@@ -4,3 +4,4 @@ logseq.cli.commands.graph/show-graph
 logseq.cli.commands.graph/list-graphs
 logseq.cli.commands.query/query
 logseq.cli.commands.search/search
+logseq.cli.commands.export/export

+ 4 - 1
deps/cli/.clj-kondo/config.edn

@@ -2,7 +2,8 @@
  {:aliased-namespace-symbol {:level :warning}
   :namespace-name-mismatch {:level :warning}
   :used-underscored-binding {:level :warning}
-  :shadowed-var {:level :warning}
+  :shadowed-var {:level :warning
+                 :exclude [meta time min meta name type]}
 
   :consistent-alias
   {:aliases {"fs" fs
@@ -12,6 +13,8 @@
              datascript.core d
              logseq.cli.commands.graph cli-graph
              logseq.cli.common.graph cli-common-graph
+             logseq.cli.common.export.text cli-export-text
+            ;;  logseq.cli.common.export.common cli-export-common
              logseq.cli.util cli-util
              logseq.cli.text-util cli-text-util
              logseq.common.config common-config

+ 4 - 2
deps/cli/package.json

@@ -10,9 +10,11 @@
   },
   "license": "MIT",
   "dependencies": {
-    "@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v26",
+    "@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v27",
     "better-sqlite3": "~11.10.0",
-    "fs-extra": "^11.3.0"
+    "fs-extra": "^11.3.0",
+    "mldoc": "^1.5.9",
+    "jszip": "3.8.0"
   },
   "repository": {
     "type": "git",

+ 3 - 0
deps/cli/src/logseq/cli.cljs

@@ -76,6 +76,9 @@
     :fn (lazy-load-fn 'logseq.cli.commands.query/query)
     :args->opts [:graph :args] :coerce {:args []} :no-keyword-opts true :require [:graph]
     :spec cli-spec/query}
+   {:cmds ["export"] :desc "Export DB graph as MD"
+    :fn (lazy-load-fn 'logseq.cli.commands.export/export)
+    :args->opts [:graph] :require [:graph]}
    {:cmds ["export-edn"] :desc "Export DB graph as EDN"
     :fn (lazy-load-fn 'logseq.cli.commands.export-edn/export)
     :args->opts [:graph] :require [:graph]

+ 75 - 0
deps/cli/src/logseq/cli/commands/export.cljs

@@ -0,0 +1,75 @@
+(ns logseq.cli.commands.export
+  "Export command"
+  (:require ["fs" :as fs]
+            [cljs.pprint]
+            [clojure.string :as string]
+            [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.file :as cli-common-file]
+            [logseq.cli.common.zip :as cli-common-zip]
+            [logseq.cli.util :as cli-util]
+            [logseq.common.config :as common-config]
+            [logseq.common.util :as common-util]
+            [logseq.db.common.entity-plus :as entity-plus]
+            [logseq.db.common.sqlite-cli :as sqlite-cli]
+            [logseq.db.sqlite.create-graph :as sqlite-create-graph]))
+
+(defn get-all-page->content
+  [repo db options]
+  ;; TODO: entity-plus or sqlite-util load faster?
+  (let [filter-fn (if (entity-plus/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)
+                 (cli-common-file/block->content repo db (:block/uuid e) {} options)])))))
+
+(defn <get-file-contents
+  [repo db suffix]
+  ;; TODO: p/let
+  (let [page->content (get-all-page->content repo
+                                             db
+                                             ;; TODO: Indentation
+                                             {:export-bullet-indentation "\t"})]
+    (map (fn [[page-title content]]
+           {:path (str page-title "." suffix)
+            :content content
+            :title page-title
+            :format :markdown})
+         page->content)))
+
+(defn export-files-as-markdown
+  "options see also `export-blocks-as-markdown`"
+  [repo files options]
+  (mapv
+   (fn [{:keys [path title content]}]
+     [(or path title) (cli-export-text/export-helper repo content :markdown options)])
+   files))
+
+(defn export-repo-as-markdown!
+  [repo db]
+  (let [files* (<get-file-contents repo db "md")]
+    (when (seq files*)
+      (let [files (binding [cli-export-common/*current-db* db
+                            cli-export-common/*current-repo* repo
+                            cli-export-common/*content-config* {:export-bullet-indentation "\t"}]
+                    (export-files-as-markdown repo files* nil))
+            repo' (string/replace repo common-config/db-version-prefix "")
+            zip-file-name (str repo' "_markdown_" (quot (common-util/time-ms) 1000))]
+        (prn :files files)
+        (let [zip (cli-common-zip/make-zip zip-file-name files repo')]
+          (-> (.generateNodeStream zip #js {:streamFiles true :type "nodebuffer"})
+              (.pipe (fs/createWriteStream (str zip-file-name ".zip"))))
+          (println "Exported graph to" (str zip-file-name ".zip")))))))
+
+(defn export [{{:keys [graph]} :opts}]
+  (if (fs/existsSync (cli-util/get-graph-dir graph))
+    (let [conn (apply sqlite-cli/open-db! (cli-util/->open-db-args graph))]
+      (export-repo-as-markdown! (str common-config/db-version-prefix graph) @conn))
+    (cli-util/error "Graph" (pr-str graph) "does not exist")))

+ 864 - 0
deps/cli/src/logseq/cli/common/export/common.cljs

@@ -0,0 +1,864 @@
+(ns logseq.cli.common.export.common
+  "common fns for exporting.
+  exclude some fns which produce lazy-seq, which can cause strange behaviors
+  when use together with dynamic var."
+  (:refer-clojure :exclude [map filter])
+  (:require [cljs.core.match :refer [match]]
+            [clojure.string :as string]
+            [datascript.core :as d]
+            [logseq.cli.common.file :as cli-common-file]
+            [logseq.common.util :as common-util]
+            [logseq.db :as ldb]
+            [logseq.graph-parser.mldoc :as gp-mldoc]
+            [logseq.outliner.tree :as otree]
+            [malli.core :as m]
+            [malli.util :as mu]))
+
+;;; TODO: split frontend.handler.export.text related states
+(def ^:dynamic *state*
+  "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}})
+
+;; Fn workarounds
+;; ==============
+
+(def ^:dynamic *current-db* nil)
+(def ^:dynamic *current-repo* 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
+(defn- get-blocks-contents
+  [repo root-block-uuid & {:keys [init-level]
+                           :or {init-level 1}}]
+  (let [block (d/entity *current-db* [:block/uuid root-block-uuid])
+        link (:block/link block)
+        block' (or link block)
+        root-id (:block/uuid block')
+        blocks (ldb/get-block-and-children *current-db* root-id)
+        tree (otree/blocks->vec-tree repo *current-db* blocks root-id {:link link})]
+    (cli-common-file/tree->file-content *current-repo* *current-db* tree
+                                        {:init-level init-level :link link}
+                                        *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?)
+
+(defn- block-uuid->ast
+  [block-uuid]
+  (let [block (into {} (get-block-by-uuid block-uuid))
+        content (cli-common-file/tree->file-content *current-repo* *current-db* [block] {:init-level 1} *content-config*)
+        format :markdown]
+    (when content
+      (removev Properties-block-ast?
+               (mapv remove-block-ast-pos
+                     (gp-mldoc/->edn *current-repo* content format))))))
+
+(defn- block-uuid->ast-with-children
+  [block-uuid]
+  (let [content (get-blocks-contents *current-repo* block-uuid)
+        format :markdown]
+    (when content
+      (removev Properties-block-ast?
+               (mapv remove-block-ast-pos
+                     (gp-mldoc/->edn *current-repo* content format))))))
+
+(defn get-page-content
+  [page-uuid]
+  (let [repo *current-repo*
+        db *current-db*]
+    (cli-common-file/block->content repo db page-uuid
+                                    nil
+                                    *content-config*)))
+
+(defn- page-name->ast
+  [page-name]
+  (let [page (ldb/get-page *current-db* 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
+                         (gp-mldoc/->edn *current-repo* 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 (zero-pad hour))
+        min  (when min (zero-pad min))
+        time (cond
+               (and hour min)
+               (common-util/format " %s:%s" hour min)
+               hour
+               (common-util/format " %s" hour)
+               :else
+               "")]
+    (common-util/format "%s%s-%s-%s %s%s%s%s"
+                        open
+                        (str year)
+                        (zero-pad month)
+                        (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)))
+
+;; (defn <get-all-pages
+;;   [repo]
+;;   (state/<invoke-db-worker :thread-api/export-get-all-pages repo))
+
+;; (defn <get-debug-datoms
+;;   [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
+  [{:keys [title] :as ast-content}]
+  (let [inline-coll  title
+        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)]
+    (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
+                     (vec (concat 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)

+ 502 - 0
deps/cli/src/logseq/cli/common/export/text.cljs

@@ -0,0 +1,502 @@
+(ns logseq.cli.common.export.text
+  (:require [clojure.string :as string]
+            [logseq.cli.common.export.common :as common :refer
+             [*state* newline* indent raw-text space simple-asts->string]]
+            [logseq.graph-parser.mldoc :as gp-mldoc]))
+
+(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)))
+
+(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")))))
+
+
+(defn export-helper
+  [repo 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 (gp-mldoc/->edn repo content format)
+            ast (mapv common/remove-block-ast-pos ast)
+            ast (vec (remove 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 (vec (mapcat block-ast->simple-ast ast***))]
+        (simple-asts->string simple-asts)))))

+ 123 - 0
deps/cli/src/logseq/cli/common/file.cljs

@@ -0,0 +1,123 @@
+(ns logseq.cli.common.file
+  "Convert blocks to file content. Used for exports and saving file to disk. Shared
+  by CLI, 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])]
+          #_:clj-kondo/ignore
+          (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)))

+ 22 - 0
deps/cli/src/logseq/cli/common/zip.cljs

@@ -0,0 +1,22 @@
+(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))

+ 418 - 5
deps/cli/yarn.lock

@@ -2,12 +2,22 @@
 # yarn lockfile v1
 
 
-"@logseq/nbb-logseq@github:logseq/nbb-logseq#feat-db-v26":
-  version "1.2.173-feat-db-v25"
-  resolved "https://codeload.github.com/logseq/nbb-logseq/tar.gz/b26d290944234b20762ff109e5328b87ea240692"
+"@logseq/nbb-logseq@github:logseq/nbb-logseq#feat-db-v27":
+  version "1.2.173-feat-db-v27"
+  resolved "https://codeload.github.com/logseq/nbb-logseq/tar.gz/52af92d4ccc7eed31ceb6f95ba345bd8563bb597"
   dependencies:
     import-meta-resolve "^4.1.0"
 
+ansi-regex@^2.0.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
+  integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==
+
+ansi-regex@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.1.tgz#123d6479e92ad45ad897d4054e3c7ca7db4944e1"
+  integrity sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==
+
 base64-js@^1.3.1:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
@@ -45,11 +55,51 @@ buffer@^5.5.0:
     base64-js "^1.3.1"
     ieee754 "^1.1.13"
 
+camelcase@^5.0.0:
+  version "5.3.1"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
+  integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
+
 chownr@^1.1.1:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
   integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
 
+cliui@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49"
+  integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==
+  dependencies:
+    string-width "^2.1.1"
+    strip-ansi "^4.0.0"
+    wrap-ansi "^2.0.0"
+
+code-point-at@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
+  integrity sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==
+
+core-util-is@~1.0.0:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
+  integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
+
+cross-spawn@^6.0.0:
+  version "6.0.6"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.6.tgz#30d0efa0712ddb7eb5a76e1e8721bffafa6b5d57"
+  integrity sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==
+  dependencies:
+    nice-try "^1.0.4"
+    path-key "^2.0.1"
+    semver "^5.5.0"
+    shebang-command "^1.2.0"
+    which "^1.2.9"
+
+decamelize@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+  integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
+
 decompress-response@^6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
@@ -74,6 +124,19 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1:
   dependencies:
     once "^1.4.0"
 
+execa@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
+  integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==
+  dependencies:
+    cross-spawn "^6.0.0"
+    get-stream "^4.0.0"
+    is-stream "^1.1.0"
+    npm-run-path "^2.0.0"
+    p-finally "^1.0.0"
+    signal-exit "^3.0.0"
+    strip-eof "^1.0.0"
+
 expand-template@^2.0.3:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
@@ -84,6 +147,13 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
   integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
 
+find-up@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
+  integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
+  dependencies:
+    locate-path "^3.0.0"
+
 fs-constants@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
@@ -98,6 +168,18 @@ fs-extra@^11.3.0:
     jsonfile "^6.0.1"
     universalify "^2.0.0"
 
+get-caller-file@^1.0.1:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
+  integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==
+
+get-stream@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
+  integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
+  dependencies:
+    pump "^3.0.0"
+
 [email protected]:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
@@ -113,12 +195,17 @@ ieee754@^1.1.13:
   resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
   integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
 
+immediate@~3.0.5:
+  version "3.0.6"
+  resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
+  integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==
+
 import-meta-resolve@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz#f9db8bead9fafa61adb811db77a2bf22c5399706"
   integrity sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==
 
-inherits@^2.0.3, inherits@^2.0.4:
+inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
   integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -128,6 +215,38 @@ ini@~1.3.0:
   resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
   integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
 
+invert-kv@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02"
+  integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==
+
+is-fullwidth-code-point@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
+  integrity sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==
+  dependencies:
+    number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+  integrity sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==
+
+is-stream@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
+  integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==
+
+isarray@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+  integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
+
+isexe@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+  integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
+
 jsonfile@^6.0.1:
   version "6.1.0"
   resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
@@ -137,6 +256,59 @@ jsonfile@^6.0.1:
   optionalDependencies:
     graceful-fs "^4.1.6"
 
[email protected]:
+  version "3.8.0"
+  resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.8.0.tgz#a2ac3c33fe96a76489765168213655850254d51b"
+  integrity sha512-cnpQrXvFSLdsR9KR5/x7zdf6c3m8IhZfZzSblFEHSqBaVwD2nvJ4CuCKLyvKvwBgZm08CgfSoiTBQLm5WW9hGw==
+  dependencies:
+    lie "~3.3.0"
+    pako "~1.0.2"
+    readable-stream "~2.3.6"
+    set-immediate-shim "~1.0.1"
+
+lcid@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf"
+  integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==
+  dependencies:
+    invert-kv "^2.0.0"
+
+lie@~3.3.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a"
+  integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==
+  dependencies:
+    immediate "~3.0.5"
+
+locate-path@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
+  integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
+  dependencies:
+    p-locate "^3.0.0"
+    path-exists "^3.0.0"
+
+map-age-cleaner@^0.1.1:
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a"
+  integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==
+  dependencies:
+    p-defer "^1.0.0"
+
+mem@^4.0.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178"
+  integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==
+  dependencies:
+    map-age-cleaner "^0.1.1"
+    mimic-fn "^2.0.0"
+    p-is-promise "^2.0.0"
+
+mimic-fn@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
+  integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
+
 mimic-response@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
@@ -152,11 +324,23 @@ mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
   resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
   integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
 
+mldoc@^1.5.9:
+  version "1.5.9"
+  resolved "https://registry.yarnpkg.com/mldoc/-/mldoc-1.5.9.tgz#43d740351c64285f0f4988ac9497922d54ae66fc"
+  integrity sha512-87FQ7hseS87tsk+VdpIigpu8LH+GwmbbFgpxgFwvnbH5oOjmIrc47laH4Dyggzqiy8/vMjDHkl7vsId0eXhCDQ==
+  dependencies:
+    yargs "^12.0.2"
+
 napi-build-utils@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz#13c22c0187fcfccce1461844136372a47ddc027e"
   integrity sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==
 
+nice-try@^1.0.4:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
+  integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
+
 node-abi@^3.3.0:
   version "3.75.0"
   resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.75.0.tgz#2f929a91a90a0d02b325c43731314802357ed764"
@@ -164,6 +348,18 @@ node-abi@^3.3.0:
   dependencies:
     semver "^7.3.5"
 
+npm-run-path@^2.0.0:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
+  integrity sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==
+  dependencies:
+    path-key "^2.0.0"
+
+number-is-nan@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
+  integrity sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==
+
 once@^1.3.1, once@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
@@ -171,6 +367,64 @@ once@^1.3.1, once@^1.4.0:
   dependencies:
     wrappy "1"
 
+os-locale@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a"
+  integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==
+  dependencies:
+    execa "^1.0.0"
+    lcid "^2.0.0"
+    mem "^4.0.0"
+
+p-defer@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
+  integrity sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==
+
+p-finally@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+  integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==
+
+p-is-promise@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e"
+  integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==
+
+p-limit@^2.0.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
+  integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
+  dependencies:
+    p-try "^2.0.0"
+
+p-locate@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
+  integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
+  dependencies:
+    p-limit "^2.0.0"
+
+p-try@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+  integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+
+pako@~1.0.2:
+  version "1.0.11"
+  resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
+  integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
+
+path-exists@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+  integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==
+
+path-key@^2.0.0, path-key@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+  integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==
+
 prebuild-install@^7.1.1:
   version "7.1.3"
   resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.3.tgz#d630abad2b147443f20a212917beae68b8092eec"
@@ -189,6 +443,11 @@ prebuild-install@^7.1.1:
     tar-fs "^2.0.0"
     tunnel-agent "^0.6.0"
 
+process-nextick-args@~2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
+  integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
+
 pump@^3.0.0:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.3.tgz#151d979f1a29668dc0025ec589a455b53282268d"
@@ -216,16 +475,76 @@ readable-stream@^3.1.1, readable-stream@^3.4.0:
     string_decoder "^1.1.1"
     util-deprecate "^1.0.1"
 
+readable-stream@~2.3.6:
+  version "2.3.8"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b"
+  integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==
+  dependencies:
+    core-util-is "~1.0.0"
+    inherits "~2.0.3"
+    isarray "~1.0.0"
+    process-nextick-args "~2.0.0"
+    safe-buffer "~5.1.1"
+    string_decoder "~1.1.1"
+    util-deprecate "~1.0.1"
+
+require-directory@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+  integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
+
+require-main-filename@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
+  integrity sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==
+
 safe-buffer@^5.0.1, safe-buffer@~5.2.0:
   version "5.2.1"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
   integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
 
+safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+  integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+
+semver@^5.5.0:
+  version "5.7.2"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8"
+  integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==
+
 semver@^7.3.5:
   version "7.7.2"
   resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
   integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
 
+set-blocking@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+  integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
+
+set-immediate-shim@~1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61"
+  integrity sha512-Li5AOqrZWCVA2n5kryzEmqai6bKSIvpz5oUJHPVj6+dsbD3X1ixtsY5tEnsaNpH3pFAHmG8eIHUrtEtohrg+UQ==
+
+shebang-command@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+  integrity sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==
+  dependencies:
+    shebang-regex "^1.0.0"
+
+shebang-regex@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+  integrity sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==
+
+signal-exit@^3.0.0:
+  version "3.0.7"
+  resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
+  integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
+
 simple-concat@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f"
@@ -240,6 +559,23 @@ simple-get@^4.0.0:
     once "^1.3.1"
     simple-concat "^1.0.0"
 
+string-width@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
+  integrity sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==
+  dependencies:
+    code-point-at "^1.0.0"
+    is-fullwidth-code-point "^1.0.0"
+    strip-ansi "^3.0.0"
+
+string-width@^2.0.0, string-width@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
+  integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
+  dependencies:
+    is-fullwidth-code-point "^2.0.0"
+    strip-ansi "^4.0.0"
+
 string_decoder@^1.1.1:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
@@ -247,6 +583,32 @@ string_decoder@^1.1.1:
   dependencies:
     safe-buffer "~5.2.0"
 
+string_decoder@~1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
+  integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
+  dependencies:
+    safe-buffer "~5.1.0"
+
+strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
+  integrity sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==
+  dependencies:
+    ansi-regex "^2.0.0"
+
+strip-ansi@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
+  integrity sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==
+  dependencies:
+    ansi-regex "^3.0.0"
+
+strip-eof@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
+  integrity sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==
+
 strip-json-comments@~2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
@@ -285,12 +647,63 @@ universalify@^2.0.0:
   resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
   integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
 
-util-deprecate@^1.0.1:
+util-deprecate@^1.0.1, util-deprecate@~1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
   integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
 
+which-module@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409"
+  integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==
+
+which@^1.2.9:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+  integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
+  dependencies:
+    isexe "^2.0.0"
+
+wrap-ansi@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
+  integrity sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==
+  dependencies:
+    string-width "^1.0.1"
+    strip-ansi "^3.0.1"
+
 wrappy@1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
   integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
+
+"y18n@^3.2.1 || ^4.0.0":
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
+  integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
+
+yargs-parser@^11.1.1:
+  version "11.1.1"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4"
+  integrity sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==
+  dependencies:
+    camelcase "^5.0.0"
+    decamelize "^1.2.0"
+
+yargs@^12.0.2:
+  version "12.0.5"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13"
+  integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==
+  dependencies:
+    cliui "^4.0.0"
+    decamelize "^1.2.0"
+    find-up "^3.0.0"
+    get-caller-file "^1.0.1"
+    os-locale "^3.0.0"
+    require-directory "^2.1.1"
+    require-main-filename "^1.0.1"
+    set-blocking "^2.0.0"
+    string-width "^2.0.0"
+    which-module "^2.0.0"
+    y18n "^3.2.1 || ^4.0.0"
+    yargs-parser "^11.1.1"

+ 1 - 1
deps/common/package.json

@@ -3,7 +3,7 @@
   "version": "1.0.0",
   "private": true,
   "devDependencies": {
-    "@logseq/nbb-logseq": "logseq/nbb-logseq#feat-db-v26"
+    "@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v27"
   },
   "scripts": {
     "test": "yarn nbb-logseq -cp test -m nextjournal.test-runner"

+ 3 - 3
deps/common/yarn.lock

@@ -2,9 +2,9 @@
 # yarn lockfile v1
 
 
-"@logseq/nbb-logseq@logseq/nbb-logseq#feat-db-v26":
-  version "1.2.173-feat-db-v25"
-  resolved "https://codeload.github.com/logseq/nbb-logseq/tar.gz/b26d290944234b20762ff109e5328b87ea240692"
+"@logseq/nbb-logseq@github:logseq/nbb-logseq#feat-db-v27":
+  version "1.2.173-feat-db-v27"
+  resolved "https://codeload.github.com/logseq/nbb-logseq/tar.gz/52af92d4ccc7eed31ceb6f95ba345bd8563bb597"
   dependencies:
     import-meta-resolve "^4.1.0"
 

+ 1 - 1
deps/db/package.json

@@ -3,7 +3,7 @@
   "version": "1.0.0",
   "private": true,
   "devDependencies": {
-    "@logseq/nbb-logseq": "logseq/nbb-logseq#feat-db-v26"
+    "@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v27"
   },
   "dependencies": {
     "better-sqlite3": "11.10.0"

+ 2 - 2
deps/db/src/logseq/db.cljs

@@ -136,8 +136,8 @@
              :as opts}]
   (if-let [children (sort-by-order
                      (if include-property-block?
-                       (:block/_raw-parent entity)
-                       (:block/_parent entity)))]
+                       (entity-plus/lookup-kv-then-entity entity :block/_raw-parent)
+                       (entity-plus/lookup-kv-then-entity entity :block/_parent)))]
     (cons entity (mapcat #(get-block-and-children-aux % opts) children))
     [entity]))
 

+ 3 - 3
deps/db/yarn.lock

@@ -2,9 +2,9 @@
 # yarn lockfile v1
 
 
-"@logseq/nbb-logseq@logseq/nbb-logseq#feat-db-v26":
-  version "1.2.173-feat-db-v25"
-  resolved "https://codeload.github.com/logseq/nbb-logseq/tar.gz/b26d290944234b20762ff109e5328b87ea240692"
+"@logseq/nbb-logseq@github:logseq/nbb-logseq#feat-db-v27":
+  version "1.2.173-feat-db-v27"
+  resolved "https://codeload.github.com/logseq/nbb-logseq/tar.gz/52af92d4ccc7eed31ceb6f95ba345bd8563bb597"
   dependencies:
     import-meta-resolve "^4.1.0"
 

+ 1 - 1
deps/graph-parser/package.json

@@ -3,7 +3,7 @@
   "version": "1.0.0",
   "private": true,
   "devDependencies": {
-    "@logseq/nbb-logseq": "logseq/nbb-logseq#feat-db-v26",
+    "@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v27",
     "better-sqlite3": "11.10.0"
   },
   "dependencies": {

+ 3 - 3
deps/graph-parser/yarn.lock

@@ -2,9 +2,9 @@
 # yarn lockfile v1
 
 
-"@logseq/nbb-logseq@logseq/nbb-logseq#feat-db-v26":
-  version "1.2.173-feat-db-v25"
-  resolved "https://codeload.github.com/logseq/nbb-logseq/tar.gz/b26d290944234b20762ff109e5328b87ea240692"
+"@logseq/nbb-logseq@github:logseq/nbb-logseq#feat-db-v27":
+  version "1.2.173-feat-db-v27"
+  resolved "https://codeload.github.com/logseq/nbb-logseq/tar.gz/52af92d4ccc7eed31ceb6f95ba345bd8563bb597"
   dependencies:
     import-meta-resolve "^4.1.0"
 

+ 1 - 1
deps/outliner/package.json

@@ -3,7 +3,7 @@
   "version": "1.0.0",
   "private": true,
   "devDependencies": {
-    "@logseq/nbb-logseq": "logseq/nbb-logseq#feat-db-v26"
+    "@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v27"
   },
   "dependencies": {
     "better-sqlite3": "11.10.0",

+ 3 - 3
deps/outliner/yarn.lock

@@ -2,9 +2,9 @@
 # yarn lockfile v1
 
 
-"@logseq/nbb-logseq@logseq/nbb-logseq#feat-db-v26":
-  version "1.2.173-feat-db-v25"
-  resolved "https://codeload.github.com/logseq/nbb-logseq/tar.gz/b26d290944234b20762ff109e5328b87ea240692"
+"@logseq/nbb-logseq@github:logseq/nbb-logseq#feat-db-v27":
+  version "1.2.173-feat-db-v27"
+  resolved "https://codeload.github.com/logseq/nbb-logseq/tar.gz/52af92d4ccc7eed31ceb6f95ba345bd8563bb597"
   dependencies:
     import-meta-resolve "^4.1.0"
 

+ 1 - 1
deps/publishing/package.json

@@ -3,7 +3,7 @@
   "version": "1.0.0",
   "private": true,
   "devDependencies": {
-    "@logseq/nbb-logseq": "logseq/nbb-logseq#feat-db-v26",
+    "@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v27",
     "mldoc": "^1.5.9"
   },
   "dependencies": {

+ 3 - 3
deps/publishing/yarn.lock

@@ -2,9 +2,9 @@
 # yarn lockfile v1
 
 
-"@logseq/nbb-logseq@logseq/nbb-logseq#feat-db-v26":
-  version "1.2.173-feat-db-v25"
-  resolved "https://codeload.github.com/logseq/nbb-logseq/tar.gz/b26d290944234b20762ff109e5328b87ea240692"
+"@logseq/nbb-logseq@github:logseq/nbb-logseq#feat-db-v27":
+  version "1.2.173-feat-db-v27"
+  resolved "https://codeload.github.com/logseq/nbb-logseq/tar.gz/52af92d4ccc7eed31ceb6f95ba345bd8563bb597"
   dependencies:
     import-meta-resolve "^4.1.0"
 

+ 1 - 1
scripts/package.json

@@ -3,7 +3,7 @@
   "version": "1.0.0",
   "private": true,
   "devDependencies": {
-    "@logseq/nbb-logseq": "logseq/nbb-logseq#feat-db-v26"
+    "@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v27"
   },
   "dependencies": {
     "better-sqlite3": "11.10.0",

+ 3 - 3
scripts/yarn.lock

@@ -2,9 +2,9 @@
 # yarn lockfile v1
 
 
-"@logseq/nbb-logseq@logseq/nbb-logseq#feat-db-v26":
-  version "1.2.173-feat-db-v25"
-  resolved "https://codeload.github.com/logseq/nbb-logseq/tar.gz/b26d290944234b20762ff109e5328b87ea240692"
+"@logseq/nbb-logseq@github:logseq/nbb-logseq#feat-db-v27":
+  version "1.2.173-feat-db-v27"
+  resolved "https://codeload.github.com/logseq/nbb-logseq/tar.gz/52af92d4ccc7eed31ceb6f95ba345bd8563bb597"
   dependencies:
     import-meta-resolve "^4.1.0"