fix: update markdown mirror

This commit is contained in:
Tienson Qin
2026-05-05 20:35:11 +08:00
parent cd31a39fef
commit b5eeb82689
12 changed files with 133 additions and 32 deletions

View File

@@ -2,7 +2,6 @@
"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]

View File

@@ -82,8 +82,8 @@
[db block spaces-tabs context]
(let [block (or (when-let [id (:db/id block)]
(d/entity db id))
(when-let [uuid (:block/uuid block)]
(d/entity db [:block/uuid uuid]))
(when-let [block-uuid (:block/uuid block)]
(d/entity db [:block/uuid block-uuid]))
block)
properties (->> (db-property/properties block)
(remove (fn [[k _]]

View File

@@ -65,7 +65,7 @@ Rules:
- Contents in '/logseq/.recycle/' are ignored
- Contents in '/logseq/bak/' are ignored
- Contents in with '/logseq/version-files/' are ignored
- Contents in '/markdown-mirror/' are ignored
- Contents in '/mirror/markdown/' are ignored
"
[dir path]
(let [dir (path/path-normalize dir)
@@ -74,7 +74,7 @@ Rules:
(when (string? path)
(or
(some #(string/starts-with? rpath %)
["." "logseq/.recycle" "logseq/bak" "logseq/version-files" "markdown-mirror"])
["." "logseq/.recycle" "logseq/bak" "logseq/version-files" "mirror/markdown"])
(contains? #{"logseq/graphs-txid.edn" "logseq/pages-metadata.edn"} rpath)
(some #(string/includes? rpath (str "/" % "/"))
["node_modules"])

View File

@@ -29,9 +29,9 @@
(fs/writeFileSync "tmp/test-graph/journals/2023_05_09.md" "")
;; Create files that are ignored
(fs/mkdirSync (node-path/join "tmp/test-graph" "logseq" "bak"))
(fs/mkdirSync (node-path/join "tmp/test-graph" "markdown-mirror" "pages") #js {:recursive true})
(fs/mkdirSync (node-path/join "tmp/test-graph" "mirror" "markdown" "pages") #js {:recursive true})
(fs/writeFileSync "tmp/test-graph/logseq/bak/baz.md" "")
(fs/writeFileSync "tmp/test-graph/logseq/.gitignore" "")
(fs/writeFileSync "tmp/test-graph/markdown-mirror/pages/foo.md" "")
(fs/writeFileSync "tmp/test-graph/mirror/markdown/pages/foo.md" "")
(is (= ["tmp/test-graph/journals/2023_05_09.md" "tmp/test-graph/pages/foo.md"]
(common-graph/get-files "tmp/test-graph"))))

View File

@@ -1,7 +1,7 @@
# ADR 0016: Electron Markdown Mirror
Date: 2026-05-05
Status: Proposed
Status: Accepted
## Context
Logseq DB graphs do not expose one editable Markdown file per page in the graph
@@ -22,14 +22,14 @@ builds do not have the same graph-directory filesystem guarantees.
3. When the setting is enabled for the Electron app, Logseq writes derived
Markdown files under the current graph directory:
- journals:
`markdown-mirror/journals/<journal-file-name>.md`
`mirror/markdown/journals/<journal-file-name>.md`
- other pages:
`markdown-mirror/pages/<page-file-name>.md`
`mirror/markdown/pages/<page-file-name>.md`
4. For a graph at `~/logseq/graphs/graph-xxx`, mirror files are written under:
- `~/logseq/graphs/graph-xxx/markdown-mirror/journals/`
- `~/logseq/graphs/graph-xxx/markdown-mirror/pages/`
- `~/logseq/graphs/graph-xxx/mirror/markdown/journals/`
- `~/logseq/graphs/graph-xxx/mirror/markdown/pages/`
5. Markdown Mirror is derived output. The DB remains the source of truth.
6. Files under `markdown-mirror/**` must be ignored by graph import, file
6. Files under `mirror/markdown/**` must be ignored by graph import, file
watchers, and graph parsing so the mirror never feeds back into the graph.
7. The feature is not available in browser or mobile builds, even if a stale
setting value exists.
@@ -68,8 +68,8 @@ builds do not have the same graph-directory filesystem guarantees.
Electron app and CLI do not produce different file names for the same graph.
## Output Layout and Naming
1. Journal pages are written below `markdown-mirror/journals/`.
2. Non-journal pages are written below `markdown-mirror/pages/`.
1. Journal pages are written below `mirror/markdown/journals/`.
2. Non-journal pages are written below `mirror/markdown/pages/`.
3. Journal file names use the existing Logseq journal file-name rules for the
graph configuration.
4. Non-journal page file names use the normalized page title:
@@ -88,14 +88,14 @@ builds do not have the same graph-directory filesystem guarantees.
page is renamed or deleted. Do not renumber existing duplicate-title mirror
paths when another duplicate is created or removed.
9. The implementation keeps a per-graph mirror index under
`markdown-mirror/.index.edn`.
`mirror/markdown/.index.edn`.
10. The mirror index stores at least:
- page uuid -> relative mirror path
- relative mirror path -> page uuid
- page uuid -> last known normalized title stem
11. The mirror index is implementation metadata for path stability. It is not
graph content and must be ignored by graph import and watchers along with the
rest of `markdown-mirror/**`.
rest of `mirror/markdown/**`.
12. All mirror file names pass through a single cross-platform filename
normalizer before joining paths.
13. Duplicate journal-day entities indicate invalid graph state for the mirror.
@@ -109,7 +109,7 @@ builds do not have the same graph-directory filesystem guarantees.
16. Page deletion deletes the corresponding mirror file and removes the page uuid
from the mirror index.
17. The write guard must reject any computed path outside the graph's
`markdown-mirror/` directory.
`mirror/markdown/` directory.
18. Built-in pages and property pages are excluded from path allocation and
mirror writes. User Tag/Class pages are not excluded by this rule. If a
previously mirrored page becomes excluded, the old mirror file is removed.
@@ -209,7 +209,7 @@ builds do not have the same graph-directory filesystem guarantees.
4. Mirror files include block and page property drawers, including user
properties, with rendered property values.
5. Assets are referenced as normal exported Markdown references. This ADR does
not copy assets into `markdown-mirror/`.
not copy assets into `mirror/markdown/`.
6. Page references remain in Logseq wiki-link form, for example `[[Foo]]`.
7. Ambiguous page references caused by duplicate page titles are an accepted
limitation of Markdown Mirror. Do not rewrite page references to uuid-based
@@ -227,7 +227,7 @@ builds do not have the same graph-directory filesystem guarantees.
## Non-goals
1. Markdown Mirror is not bidirectional sync.
2. Editing files in `markdown-mirror/` does not update the graph.
2. Editing files in `mirror/markdown/` does not update the graph.
3. The mirror is not a backup format with guaranteed import fidelity.
4. The mirror does not replace existing graph export features.
5. The mirror does not support browser or mobile runtimes in this ADR.
@@ -241,7 +241,7 @@ builds do not have the same graph-directory filesystem guarantees.
- The output layout is predictable for tools that watch journals and pages
separately.
- Page file names remain readable and practical in external Markdown tools.
- Ignoring `markdown-mirror/**` prevents mirror-generated files from becoming
- Ignoring `mirror/markdown/**` prevents mirror-generated files from becoming
graph input.
### Tradeoffs
@@ -276,7 +276,7 @@ bb dev:test -v frontend.worker.markdown-mirror-test/mirror-path-collision-fails-
```
Additional checks:
- `markdown-mirror/**` is excluded from graph parsing and file watchers.
- `mirror/markdown/**` is excluded from graph parsing and file watchers.
- Editor save does not await mirror completion.
- Browser and mobile builds do not expose the setting and do not schedule mirror
jobs.

View File

@@ -130,6 +130,13 @@
:ws-url (config/db-sync-ws-url)
:http-base (config/db-sync-http-base)})
(defn- <sync-markdown-mirror-setting!
[repo]
(p/let [_ (state/load-app-user-cfgs)]
(state/<invoke-db-worker :thread-api/markdown-mirror-set-enabled
repo
(true? (get-in @state/state [:electron/user-cfgs :feature/markdown-mirror?])))))
(defn- <ensure-remote!
[repo]
(if (or (nil? repo) (= repo @remote-repo))
@@ -149,7 +156,8 @@
(record-active-request-failure! repo session-id error))))]
(set-remote-runtime! repo client session-id)
(p/let [_ (state/<invoke-db-worker :thread-api/set-db-sync-config
(current-db-sync-config))]
(current-db-sync-config))
_ (<sync-markdown-mirror-setting! repo)]
nil)
(ldb/register-transact-fn!
(fn remote-transact!

View File

@@ -11,7 +11,7 @@
(defn repo-mirror-dir
[repo]
(str (graph-dir/repo->encoded-graph-dir-name repo) "/markdown-mirror"))
(str (graph-dir/repo->encoded-graph-dir-name repo) "/mirror/markdown"))
(def ^:private invalid-file-name-chars-re
#"[<>:\"|?*\\/]")
@@ -159,8 +159,7 @@
(defn- supported-runtime?
[platform*]
(or (= :node (get-in platform* [:env :runtime]))
(and (= :browser (get-in platform* [:env :runtime]))
(= :electron (get-in platform* [:env :owner-source])))))
(= :electron (get-in platform* [:env :owner-source]))))
(defn- duplicate-journal-day?
[db journal-day]
@@ -296,7 +295,7 @@
(log/error :markdown-mirror/flush-failed
{:repo repo
:error error})))))
(or (:debounce-ms opts) 250))]
(or (:debounce-ms opts) 1000))]
(swap! *repo->flush-timeout assoc repo timeout-id))))
(defn- <run-job!

View File

@@ -1632,7 +1632,7 @@
:settings.features/journals-enable-success "Journals enabled"
:settings.features/login-prompt "To access new features before anyone else you must be an Open Collective Sponsor or Backer of Logseq and therefore log in first."
:settings.features/markdown-mirror "Markdown Mirror"
:settings.features/markdown-mirror-desc "Write a derived Markdown copy of edited pages to the graph's markdown-mirror folder. Desktop only."
:settings.features/markdown-mirror-desc "Write a derived Markdown copy of edited pages to the graph's mirror/markdown folder. Desktop only."
:settings.features/markdown-mirror-regenerate "Regenerate full mirror"
:settings.features/markdown-mirror-regenerate-error "Failed to regenerate Markdown Mirror: {1}"
:settings.features/markdown-mirror-regenerate-success "Markdown Mirror regenerated"

View File

@@ -1622,7 +1622,7 @@
:settings.features/journals-enable-success "已启用日志页"
:settings.features/login-prompt "你必须是 Logseq 的 Open Collective Sponsor 或者 Backer 才能提前使用新功能(仍在测试中),因此需要登录。"
:settings.features/markdown-mirror "Markdown 镜像"
:settings.features/markdown-mirror-desc "将已编辑页面的派生 Markdown 副本写入图谱的 markdown-mirror 文件夹。仅桌面端可用。"
:settings.features/markdown-mirror-desc "将已编辑页面的派生 Markdown 副本写入图谱的 mirror/markdown 文件夹。仅桌面端可用。"
:settings.features/markdown-mirror-regenerate "重新生成完整镜像"
:settings.features/markdown-mirror-regenerate-error "重新生成 Markdown 镜像失败:{1}"
:settings.features/markdown-mirror-regenerate-success "Markdown 镜像已重新生成"

View File

@@ -67,7 +67,8 @@
(reset! persist-db/remote-repo nil)
(reset! persist-db/remote-runtime-state nil)
(reset! state/*db-worker nil)
(reset! ldb/*transact-fn nil))
(reset! ldb/*transact-fn nil)
(swap! state/state assoc :electron/user-cfgs {}))
(defn- success-body
[result]
@@ -104,6 +105,10 @@
(p/resolved {:status 200
:body (success-body nil)})
"thread-api/markdown-mirror-set-enabled"
(p/resolved {:status 200
:body (success-body nil)})
"thread-api/list-db"
(let [result (first @results)]
(swap! results #(vec (rest %)))
@@ -342,6 +347,92 @@
(set! config/db-sync-http-base original-http)
(done)))))))
(deftest electron-ensure-remote-pushes-markdown-mirror-setting-on-start-test
(async done
(let [worker-calls (atom [])
ensure-remote! #'persist-db/<ensure-remote!
original-state @state/state
original-ipc ipc/ipc
original-start! remote/start!
original-stop! remote/stop!]
(reset-runtime-state!)
(reset! state/state (assoc-in original-state
[:electron/user-cfgs :feature/markdown-mirror?]
true))
(set! ipc/ipc (fn [channel repo]
(is (= "db-worker-runtime" channel))
(p/resolved {:base-url "http://127.0.0.1:9101"
:auth-token nil
:repo repo})))
(set! remote/start! (fn [{:keys [repo]}]
(->FakeRemote repo
(fn [qkw & args]
(swap! worker-calls conj [qkw args])
(p/resolved nil)))))
(set! remote/stop! (fn [_] (p/resolved true)))
(-> (p/let [_ (ensure-remote! "logseq_db_graph_a")]
(is (= [:thread-api/markdown-mirror-set-enabled
["logseq_db_graph_a" true]]
(first (filter #(= :thread-api/markdown-mirror-set-enabled (first %))
@worker-calls)))))
(p/catch (fn [e]
(is false (str "unexpected error: " e))))
(p/finally (fn []
(reset! state/state original-state)
(set! ipc/ipc original-ipc)
(set! remote/start! original-start!)
(set! remote/stop! original-stop!)
(done)))))))
(deftest electron-ensure-remote-loads-markdown-mirror-setting-before-sync-test
(async done
(let [ipc-calls (atom [])
worker-calls (atom [])
ensure-remote! #'persist-db/<ensure-remote!
original-state @state/state
original-electron? util/electron?
original-ipc ipc/ipc
original-start! remote/start!
original-stop! remote/stop!]
(reset-runtime-state!)
(swap! state/state assoc :electron/user-cfgs nil)
(set! util/electron? (constantly true))
(set! ipc/ipc (fn [channel & args]
(swap! ipc-calls conj (into [channel] args))
(case channel
"db-worker-runtime"
(p/resolved {:base-url "http://127.0.0.1:9101"
:auth-token nil
:repo (first args)})
:userAppCfgs
(p/resolved {:feature/markdown-mirror? true})
(p/resolved nil))))
(set! remote/start! (fn [{:keys [repo]}]
(->FakeRemote repo
(fn [qkw & args]
(swap! worker-calls conj [qkw args])
(p/resolved nil)))))
(set! remote/stop! (fn [_] (p/resolved true)))
(-> (p/let [_ (ensure-remote! "logseq_db_graph_a")]
(is (= [["db-worker-runtime" "logseq_db_graph_a"]
[:userAppCfgs]]
@ipc-calls))
(is (= [:thread-api/markdown-mirror-set-enabled
["logseq_db_graph_a" true]]
(first (filter #(= :thread-api/markdown-mirror-set-enabled (first %))
@worker-calls)))))
(p/catch (fn [e]
(is false (str "unexpected error: " e))))
(p/finally (fn []
(reset! state/state original-state)
(set! util/electron? original-electron?)
(set! ipc/ipc original-ipc)
(set! remote/start! original-start!)
(set! remote/stop! original-stop!)
(done)))))))
(deftest electron-list-db-without-current-repo-does-not-bootstrap-runtime
(async done
(let [ipc-calls (atom [])

View File

@@ -41,6 +41,10 @@
(apply f args)
(p/resolved ::missing-mirror-repo-fn)))
(deftest repo-mirror-dir-is-under-mirror-markdown-test
(is (= "graph-xxx/mirror/markdown"
(markdown-mirror/repo-mirror-dir test-repo))))
(deftest normalize-file-name-is-cross-platform-and-deterministic-test
(testing "invalid filesystem characters and path separators are replaced"
(is (= "A_B_C_D_E_F_G_H"

View File

@@ -82,7 +82,7 @@
(let [root-dir (node-helper/create-tmp-dir "platform-node-text-files")]
(-> (p/let [platform (platform-node/node-platform {:root-dir root-dir})
storage (:storage platform)
path "graph-a/markdown-mirror/pages/page.md"
path "graph-a/mirror/markdown/pages/page.md"
_ ((:write-text-atomic! storage) path "mirror")
content ((:read-text! storage) path)
_ ((:delete-file! storage) path)
@@ -92,7 +92,7 @@
(is (nil? deleted-content))
(is (empty? (filter #(string/includes? % ".tmp-")
(array-seq (fs/readdirSync
(node-path/join root-dir "graphs" "graph-a" "markdown-mirror" "pages")))))))
(node-path/join root-dir "graphs" "graph-a" "mirror" "markdown" "pages")))))))
(p/catch (fn [e]
(is false (str "unexpected error: " e))))
(p/finally done)))))