mirror of
https://github.com/logseq/logseq.git
synced 2026-05-24 12:44:22 +00:00
feat: sync markdown mirror edits
Add Chokidar-backed Markdown Mirror file watching in the DB worker and start the watcher when Markdown Mirror is enabled. Regenerate mirror files on enable so external editors see current content immediately. Import a constrained set of mirror edits back into DB graphs: existing block content edits, new block insertion, block removal by omitted markers, page and journal creation from new files, page refs, and inline tags. Ignore mirror property edits and dangerous file delete/move events. Keep the importer strict: only Logseq '- ' list items are writable block boundaries, indented Markdown stays as block content, fenced code lines cannot create blocks, and unsupported top-level Markdown is rejected. Update ADR 0016 with this contract. Verified with: rtk bb dev:test -v frontend.worker.markdown-mirror-test; rtk bb dev:test -v frontend.worker.db-core-test/db-core-registers-all-thread-apis-test; Chokidar require smoke test from resources/.
This commit is contained in:
@@ -28,9 +28,11 @@ builds do not have the same graph-directory filesystem guarantees.
|
||||
4. For a graph at `~/logseq/graphs/graph-xxx`, mirror files are written under:
|
||||
- `~/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 `mirror/markdown/**` must be ignored by graph import, file
|
||||
watchers, and graph parsing so the mirror never feeds back into the graph.
|
||||
5. Markdown Mirror is derived output. The DB remains the source of truth for
|
||||
unsupported or ambiguous edits.
|
||||
6. Files under `mirror/markdown/**` must be ignored by the normal graph import,
|
||||
file watchers, and graph parsing. The only path from mirror files back into
|
||||
the graph is the dedicated Markdown Mirror watcher described below.
|
||||
7. The feature is not available in browser or mobile builds, even if a stale
|
||||
setting value exists.
|
||||
8. Settings exposes an explicit "Regenerate full mirror" action that asks the
|
||||
@@ -196,16 +198,16 @@ builds do not have the same graph-directory filesystem guarantees.
|
||||
8. Full regeneration is an explicit Settings action. The renderer only sends a
|
||||
worker request; page selection, rendering, and filesystem writes stay in the
|
||||
DB worker.
|
||||
9. Enabling the setting starts incremental mirroring for subsequent page edits.
|
||||
It does not implicitly run full regeneration.
|
||||
9. Enabling the setting starts the Markdown Mirror watcher and runs a full
|
||||
regeneration so external editors have current files immediately.
|
||||
|
||||
## Markdown Content
|
||||
1. Reuse the existing page-to-Markdown export pipeline used by worker export
|
||||
APIs instead of introducing a separate renderer-side serializer.
|
||||
2. The mirror output should match normal Markdown export semantics for page
|
||||
content.
|
||||
3. Mirror files do not include Logseq-internal mirror metadata in the Markdown
|
||||
body.
|
||||
3. Mirror files include HTML comments for page and block identity. These
|
||||
comments are the only supported identity markers for file-to-DB imports.
|
||||
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
|
||||
@@ -215,6 +217,36 @@ builds do not have the same graph-directory filesystem guarantees.
|
||||
limitation of Markdown Mirror. Do not rewrite page references to uuid-based
|
||||
links or relative Markdown links in this ADR.
|
||||
|
||||
## File-to-DB Import Contract
|
||||
1. The mirror supports a constrained two-way sync path for DB graphs on Electron.
|
||||
It is not a general Markdown importer.
|
||||
2. The only writable structural unit is a Logseq block rendered as a `- ` list
|
||||
item. New blocks must be inserted as `- ` items.
|
||||
3. Indented non-list Markdown under a block belongs to that block's content.
|
||||
This includes quotes, fenced code blocks, tables, and other Markdown
|
||||
continuation lines.
|
||||
4. Lines inside an indented fenced code block are always block content. A line
|
||||
like `- not a block` inside the fence must not create a Logseq child block.
|
||||
5. Unsupported top-level Markdown, for example a top-level quote or fenced code
|
||||
block that is not under a `- ` item, is rejected. Rejecting is safer than
|
||||
silently dropping it and then treating omitted block markers as deletes.
|
||||
6. Property drawer edits from mirror files are ignored. The DB remains the
|
||||
source of truth for page and block properties.
|
||||
7. File deletes, moves, and directory deletes from the mirror watcher are
|
||||
ignored. Page and block deletion from files is allowed only by removing block
|
||||
marker/list-item pairs from an existing page file.
|
||||
8. Creating a new page or journal from a new mirror file is supported only when
|
||||
the path maps to `pages/<name>.md` or `journals/YYYY_MM_DD.md` and the file
|
||||
contains no page or block markers from another graph object.
|
||||
9. `[[page]]` references in imported block content create missing pages and are
|
||||
stored using internal id refs in the DB transaction.
|
||||
10. Simple hashtag refs such as `#tag1` and page-ref hashtags such as
|
||||
`#[[tag one]]` create missing tag/class pages and attach the tag to the
|
||||
block.
|
||||
11. The watcher suppresses Logseq's own mirror writes by comparing recent written
|
||||
content, not by ignoring every file event for a path during a time window.
|
||||
A real external edit that changes file content must import immediately.
|
||||
|
||||
## Failure Handling
|
||||
1. Filesystem and path errors fail the mirror job for the affected page.
|
||||
2. Failures are logged with graph, page uuid, target path, and error details.
|
||||
@@ -226,8 +258,9 @@ builds do not have the same graph-directory filesystem guarantees.
|
||||
that graph until the graph is reopened with a valid directory.
|
||||
|
||||
## Non-goals
|
||||
1. Markdown Mirror is not bidirectional sync.
|
||||
2. Editing files in `mirror/markdown/` does not update the graph.
|
||||
1. Markdown Mirror is not a general bidirectional Markdown sync engine.
|
||||
2. Editing files in `mirror/markdown/` updates the graph only within the
|
||||
constrained import contract above.
|
||||
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.
|
||||
@@ -251,7 +284,8 @@ builds do not have the same graph-directory filesystem guarantees.
|
||||
- Duplicate page references such as `[[Foo]]` remain ambiguous in mirror output.
|
||||
- The first version does not backfill every existing page automatically when the
|
||||
setting is enabled; users run full regeneration explicitly.
|
||||
- External edits to mirror files are overwritten by later Logseq edits.
|
||||
- External edits outside the constrained import contract are rejected or
|
||||
overwritten by later Logseq edits.
|
||||
- Property pages are intentionally absent from the mirror, so the output is not
|
||||
a complete DB export even though page and block property drawers are included.
|
||||
|
||||
@@ -269,6 +303,13 @@ bb dev:test -v frontend.worker.markdown-mirror-test/same-title-pages-write-disti
|
||||
bb dev:test -v frontend.worker.markdown-mirror-test/page-references-remain-wiki-links-test
|
||||
bb dev:test -v frontend.worker.markdown-mirror-test/page-mirror-exports-property-values-test
|
||||
bb dev:test -v frontend.worker.markdown-mirror-test/page-mirror-exports-page-property-values-test
|
||||
bb dev:test -v frontend.worker.markdown-mirror-test/two-way-file-edit-can-change-generated-block-content-immediately-test
|
||||
bb dev:test -v frontend.worker.markdown-mirror-test/two-way-existing-block-page-ref-edit-creates-page-test
|
||||
bb dev:test -v frontend.worker.markdown-mirror-test/two-way-inserted-block-page-ref-creates-page-test
|
||||
bb dev:test -v frontend.worker.markdown-mirror-test/two-way-existing-block-hashtag-edit-creates-tag-test
|
||||
bb dev:test -v frontend.worker.markdown-mirror-test/two-way-existing-block-preserves-continuation-markdown-test
|
||||
bb dev:test -v frontend.worker.markdown-mirror-test/two-way-inserted-block-preserves-continuation-markdown-test
|
||||
bb dev:test -v frontend.worker.markdown-mirror-test/two-way-top-level-markdown-without-block-is-rejected-test
|
||||
bb dev:test -v frontend.worker.markdown-mirror-test/full-regeneration-writes-existing-non-built-in-non-property-pages-test
|
||||
bb dev:test -v frontend.worker.markdown-mirror-test/invalid-filename-characters-are-normalized-test
|
||||
bb dev:test -v frontend.worker.markdown-mirror-test/windows-reserved-filename-fails-with-diagnostic-test
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"@js-joda/core": "3.2.0",
|
||||
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||
"abort-controller": "3.0.0",
|
||||
"chokidar": "4.0.3",
|
||||
"command-exists": "1.2.9",
|
||||
"diff-match-patch": "1.0.5",
|
||||
"electron-dl": "4.0.0",
|
||||
|
||||
17
resources/pnpm-lock.yaml
generated
17
resources/pnpm-lock.yaml
generated
@@ -23,6 +23,9 @@ importers:
|
||||
abort-controller:
|
||||
specifier: 3.0.0
|
||||
version: 3.0.0
|
||||
chokidar:
|
||||
specifier: 4.0.3
|
||||
version: 4.0.3
|
||||
command-exists:
|
||||
specifier: 1.2.9
|
||||
version: 1.2.9
|
||||
@@ -469,6 +472,10 @@ packages:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
chokidar@4.0.3:
|
||||
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||
engines: {node: '>= 14.16.0'}
|
||||
|
||||
chownr@1.1.4:
|
||||
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
|
||||
|
||||
@@ -1701,6 +1708,10 @@ packages:
|
||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
readdirp@4.1.2:
|
||||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||
engines: {node: '>= 14.18.0'}
|
||||
|
||||
real-require@0.2.0:
|
||||
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
|
||||
engines: {node: '>= 12.13.0'}
|
||||
@@ -2739,6 +2750,10 @@ snapshots:
|
||||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
chokidar@4.0.3:
|
||||
dependencies:
|
||||
readdirp: 4.1.2
|
||||
|
||||
chownr@1.1.4: {}
|
||||
|
||||
chownr@3.0.0: {}
|
||||
@@ -4053,6 +4068,8 @@ snapshots:
|
||||
string_decoder: 1.3.0
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
readdirp@4.1.2: {}
|
||||
|
||||
real-require@0.2.0: {}
|
||||
|
||||
remove-accents@0.5.0: {}
|
||||
|
||||
@@ -1098,7 +1098,12 @@
|
||||
(def-thread-api :thread-api/markdown-mirror-set-enabled
|
||||
[repo enabled?]
|
||||
(markdown-mirror/set-enabled! repo enabled?)
|
||||
nil)
|
||||
(if enabled?
|
||||
(when-let [conn (worker-state/get-datascript-conn repo)]
|
||||
(p/let [_ (markdown-mirror/<start-file-watcher! repo conn {})
|
||||
result (markdown-mirror/<mirror-repo! repo @conn {})]
|
||||
result))
|
||||
(p/resolved (markdown-mirror/stop-file-watcher! repo))))
|
||||
|
||||
(def-thread-api :thread-api/markdown-mirror-flush
|
||||
[repo]
|
||||
|
||||
@@ -6,7 +6,13 @@
|
||||
[frontend.worker.platform :as platform]
|
||||
[lambdaisland.glogi :as log]
|
||||
[logseq.cli.common.file :as common-file]
|
||||
[logseq.common.util :as common-util]
|
||||
[logseq.common.util.date-time :as date-time-util]
|
||||
[logseq.common.uuid :as common-uuid]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db.common.order :as db-order]
|
||||
[logseq.db.frontend.class :as db-class]
|
||||
[logseq.db.frontend.content :as db-content]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(defn repo-mirror-dir
|
||||
@@ -28,10 +34,22 @@
|
||||
(map #(str "LPT" %) (range 1 10)))))
|
||||
|
||||
(def ^:private max-file-stem-length 160)
|
||||
(def ^:private page-marker-re #"^\s*<!--\s*logseq:page\s+([0-9a-fA-F-]{36})\s*-->\s*$")
|
||||
(def ^:private block-marker-re #"^\s*<!--\s*logseq:block\s+([0-9a-fA-F-]{36})\s*-->\s*$")
|
||||
(def ^:private markdown-block-re #"^(\s*)-\s?(.*)$")
|
||||
(def ^:private property-line-re #"^\s*[^:\s][^:]*::\s?.*$")
|
||||
(def ^:private ref-or-tag-re #"(#?)\[\[([^\[\]]+)\]\]")
|
||||
(def ^:private simple-hashtag-re #"(?i)(^|\s)#([^\s#\[\]\(\),.;:'\"`]+)")
|
||||
(def ^:private journal-relative-path-re #"^journals/(\d{4})_(\d{2})_(\d{2})\.md$")
|
||||
(def ^:private page-relative-path-re #"^pages/(.+)\.md$")
|
||||
|
||||
(defonce ^:private *repo->enabled? (atom {}))
|
||||
(defonce ^:private *repo->queued-page-jobs (atom {}))
|
||||
(defonce ^:private *repo->flush-timeout (atom {}))
|
||||
(defonce ^:private *repo->file-watchers (atom {}))
|
||||
(defonce ^:private *repo->recent-writes (atom {}))
|
||||
|
||||
(declare <read-text <write-if-changed! supported-runtime?)
|
||||
|
||||
(defn- normalize-unicode
|
||||
[s]
|
||||
@@ -82,10 +100,10 @@
|
||||
(not (ldb/journal? %))))
|
||||
(sort-by (comp str :block/uuid)))
|
||||
index (inc (or (first (keep-indexed
|
||||
(fn [idx p]
|
||||
(when (= (:block/uuid page) (:block/uuid p))
|
||||
idx))
|
||||
duplicate-pages))
|
||||
(fn [idx p]
|
||||
(when (= (:block/uuid page) (:block/uuid p))
|
||||
idx))
|
||||
duplicate-pages))
|
||||
0))
|
||||
stem' (if (= 1 index)
|
||||
stem
|
||||
@@ -96,6 +114,769 @@
|
||||
[repo relative-path]
|
||||
(str (repo-mirror-dir repo) "/" relative-path))
|
||||
|
||||
(defn- normalize-watch-path
|
||||
[path]
|
||||
(some-> path str (string/replace "\\" "/")))
|
||||
|
||||
(defn- markdown-relative-path?
|
||||
[relative-path]
|
||||
(boolean
|
||||
(or (re-matches page-relative-path-re relative-path)
|
||||
(re-matches journal-relative-path-re relative-path))))
|
||||
|
||||
(defn- watch-root-path
|
||||
[platform* repo]
|
||||
(let [relative-root (repo-mirror-dir repo)]
|
||||
(if-let [f (get-in platform* [:storage :resolve-text-path])]
|
||||
(f relative-root)
|
||||
relative-root)))
|
||||
|
||||
(defn- watcher-event-relative-path
|
||||
[repo watch-root path]
|
||||
(let [path' (normalize-watch-path path)
|
||||
watch-root' (normalize-watch-path watch-root)
|
||||
mirror-dir' (normalize-watch-path (repo-mirror-dir repo))
|
||||
prefix (str watch-root' "/")
|
||||
mirror-prefix (str mirror-dir' "/")
|
||||
relative-path (cond
|
||||
(and watch-root'
|
||||
(string/starts-with? path' prefix))
|
||||
(subs path' (count prefix))
|
||||
|
||||
(string/starts-with? path' mirror-prefix)
|
||||
(subs path' (count mirror-prefix))
|
||||
|
||||
:else
|
||||
path')]
|
||||
(when (and relative-path (markdown-relative-path? relative-path))
|
||||
relative-path)))
|
||||
|
||||
(defn- mark-recent-write!
|
||||
[repo path content]
|
||||
(swap! *repo->recent-writes assoc-in [repo path] {:time (common-util/time-ms)
|
||||
:content content}))
|
||||
|
||||
(defn- recent-written-content
|
||||
[repo path ttl-ms]
|
||||
(when-let [{:keys [time content]} (get-in @*repo->recent-writes [repo path])]
|
||||
(if (< (- (common-util/time-ms) time) ttl-ms)
|
||||
content
|
||||
(do
|
||||
(swap! *repo->recent-writes update repo dissoc path)
|
||||
nil))))
|
||||
|
||||
(defn- parse-uuid-safe
|
||||
[s]
|
||||
(try
|
||||
(uuid s)
|
||||
(catch :default _e
|
||||
nil)))
|
||||
|
||||
(defn- line-level
|
||||
[spaces]
|
||||
(inc (quot (count spaces) 2)))
|
||||
|
||||
(defn- strip-prefix-spaces
|
||||
[line n]
|
||||
(if (and (<= n (count line))
|
||||
(every? #(= \space %) (subs line 0 n)))
|
||||
(subs line n)
|
||||
line))
|
||||
|
||||
(defn- fenced-code-boundary?
|
||||
[line]
|
||||
(let [line (string/triml line)]
|
||||
(or (string/starts-with? line "```")
|
||||
(string/starts-with? line "~~~"))))
|
||||
|
||||
(defn- append-block-line
|
||||
[blocks idx line]
|
||||
(update-in blocks [idx :title] #(str % "\n" line)))
|
||||
|
||||
(defn- continuation-line
|
||||
[line continuation-indent]
|
||||
(let [prefix (apply str (repeat continuation-indent " "))]
|
||||
(when (string/starts-with? line prefix)
|
||||
(strip-prefix-spaces line continuation-indent))))
|
||||
|
||||
(defn- parse-mirror-content
|
||||
[content]
|
||||
(let [lines (string/split-lines (or content ""))]
|
||||
(loop [[line & more] lines
|
||||
pending-marker nil
|
||||
page-uuid nil
|
||||
stack []
|
||||
blocks []
|
||||
current-block-idx nil
|
||||
continuation-indent nil
|
||||
fenced-code? false]
|
||||
(if (nil? line)
|
||||
{:page-uuid page-uuid
|
||||
:blocks blocks}
|
||||
(if fenced-code?
|
||||
(if (some? current-block-idx)
|
||||
(let [line' (strip-prefix-spaces line continuation-indent)
|
||||
fenced-code?' (if (fenced-code-boundary? line')
|
||||
false
|
||||
fenced-code?)]
|
||||
(recur more pending-marker page-uuid stack
|
||||
(append-block-line blocks current-block-idx line')
|
||||
current-block-idx continuation-indent fenced-code?'))
|
||||
{:error :unsupported-top-level-markdown})
|
||||
(if-let [[_ page-uuid-str] (re-matches page-marker-re line)]
|
||||
(recur more nil (parse-uuid-safe page-uuid-str) stack blocks
|
||||
current-block-idx continuation-indent fenced-code?)
|
||||
(if-let [[_ block-uuid-str] (re-matches block-marker-re line)]
|
||||
(recur more (parse-uuid-safe block-uuid-str) page-uuid stack blocks
|
||||
current-block-idx continuation-indent fenced-code?)
|
||||
(if-let [[_ spaces title] (re-matches markdown-block-re line)]
|
||||
(let [current-level (line-level spaces)
|
||||
idx (count blocks)
|
||||
stack' (->> stack
|
||||
(remove (fn [{:keys [level]}] (>= level current-level)))
|
||||
vec)
|
||||
parent-ref (:ref (peek stack'))
|
||||
block {:uuid pending-marker
|
||||
:idx idx
|
||||
:title title
|
||||
:level current-level
|
||||
:parent-ref parent-ref}
|
||||
stack'' (conj stack' {:level current-level
|
||||
:ref (or (:uuid block) idx)})
|
||||
continuation-indent' (+ (count spaces) 2)]
|
||||
(recur more nil page-uuid stack'' (conj blocks block)
|
||||
idx continuation-indent' (fenced-code-boundary? title)))
|
||||
(cond
|
||||
(or (string/blank? line)
|
||||
(re-matches property-line-re line))
|
||||
(recur more pending-marker page-uuid stack blocks
|
||||
current-block-idx continuation-indent fenced-code?)
|
||||
|
||||
(and (some? current-block-idx)
|
||||
(some? continuation-indent)
|
||||
(continuation-line line continuation-indent))
|
||||
(let [line' (continuation-line line continuation-indent)]
|
||||
(recur more pending-marker page-uuid stack
|
||||
(append-block-line blocks current-block-idx line')
|
||||
current-block-idx continuation-indent (fenced-code-boundary? line')))
|
||||
|
||||
:else
|
||||
{:error :unsupported-top-level-markdown})))))))))
|
||||
|
||||
(defn- relative-path->new-page
|
||||
[relative-path]
|
||||
(if-let [[_ y m d] (re-matches journal-relative-path-re relative-path)]
|
||||
(let [journal-day (parse-long (str y m d))]
|
||||
{:type :journal
|
||||
:journal-day journal-day
|
||||
:title (date-time-util/int->journal-title
|
||||
journal-day
|
||||
date-time-util/default-journal-title-formatter)
|
||||
:uuid (common-uuid/gen-uuid :journal-page-uuid journal-day)})
|
||||
(when-let [[_ stem] (re-matches page-relative-path-re relative-path)]
|
||||
(when-let [title (normalize-file-stem stem)]
|
||||
{:type :page
|
||||
:title title
|
||||
:uuid (random-uuid)}))))
|
||||
|
||||
(defn- existing-page-for-path
|
||||
[db relative-path parsed-page-uuid]
|
||||
(or (when-let [[_ y m d] (re-matches journal-relative-path-re relative-path)]
|
||||
(let [journal-day (parse-long (str y m d))]
|
||||
(first (map #(d/entity db (:e %))
|
||||
(d/datoms db :avet :block/journal-day journal-day)))))
|
||||
(when-let [[_ stem] (re-matches page-relative-path-re relative-path)]
|
||||
(ldb/get-page db stem))
|
||||
(when-let [page (and parsed-page-uuid
|
||||
(d/entity db [:block/uuid parsed-page-uuid]))]
|
||||
(when (= relative-path (page-relative-path db page))
|
||||
page))))
|
||||
|
||||
(defn- content-ref-targets
|
||||
[title]
|
||||
(let [title (or title "")
|
||||
page-ref-targets (keep (fn [[_ prefix page-title]]
|
||||
(when-not (common-util/uuid-string? page-title)
|
||||
{:title page-title
|
||||
:tag? (= "#" prefix)}))
|
||||
(re-seq ref-or-tag-re title))
|
||||
simple-tag-targets (keep (fn [[_ _ tag-title]]
|
||||
(when-not (common-util/uuid-string? tag-title)
|
||||
{:title tag-title
|
||||
:tag? true}))
|
||||
(re-seq simple-hashtag-re title))]
|
||||
(distinct (concat page-ref-targets simple-tag-targets))))
|
||||
|
||||
(defn- ensure-tag-txs
|
||||
[db block-ref title page]
|
||||
(cond-> [[:db/add block-ref :block/tags :logseq.class/Tag]
|
||||
[:db/add block-ref :logseq.property.class/extends :logseq.class/Root]
|
||||
[:db/retract block-ref :block/tags :logseq.class/Page]]
|
||||
(nil? (:db/ident page))
|
||||
(conj [:db/add block-ref :db/ident (db-class/create-user-class-ident-from-name db title)])))
|
||||
|
||||
(defn- page-ref-plan
|
||||
[db title planned-pages now]
|
||||
(loop [[{target-title :title tag? :tag? :as target} & more] (content-ref-targets title)
|
||||
planned-pages planned-pages
|
||||
refs []
|
||||
tag-refs []
|
||||
page-txs []]
|
||||
(if (nil? target)
|
||||
{:planned-pages planned-pages
|
||||
:refs refs
|
||||
:page-txs page-txs
|
||||
:tag-refs tag-refs
|
||||
:db-title (db-content/title-ref->id-ref title refs {:replace-tag? true})}
|
||||
(let [page-title target-title
|
||||
page-name (common-util/page-name-sanity-lc page-title)
|
||||
planned-page (get planned-pages page-name)]
|
||||
(if planned-page
|
||||
(let [planned-pages' (if (and tag? (not (:tag? planned-page)))
|
||||
(assoc planned-pages page-name (assoc planned-page :tag? true))
|
||||
planned-pages)
|
||||
page-txs' (cond-> page-txs
|
||||
(and tag? (not (:tag? planned-page)))
|
||||
(into (ensure-tag-txs db
|
||||
[:block/uuid (:block/uuid planned-page)]
|
||||
page-title
|
||||
nil)))]
|
||||
(recur more
|
||||
planned-pages'
|
||||
(conj refs planned-page)
|
||||
(cond-> tag-refs tag? (conj planned-page))
|
||||
page-txs'))
|
||||
(if-let [page (ldb/get-page db page-title)]
|
||||
(let [ref {:block/title (:block/title page)
|
||||
:block/uuid (:block/uuid page)
|
||||
:tag? (or tag?
|
||||
(some #(= :logseq.class/Tag (:db/ident %)) (:block/tags page)))}
|
||||
page-txs' (cond-> page-txs
|
||||
tag?
|
||||
(into (ensure-tag-txs db (:db/id page) page-title page)))]
|
||||
(recur more
|
||||
(assoc planned-pages page-name ref)
|
||||
(conj refs ref)
|
||||
(cond-> tag-refs tag? (conj ref))
|
||||
page-txs'))
|
||||
(let [page-uuid (random-uuid)
|
||||
ref {:block/title page-title
|
||||
:block/uuid page-uuid
|
||||
:tag? tag?}
|
||||
page-tx {:block/title page-title
|
||||
:block/name page-name
|
||||
:block/uuid page-uuid
|
||||
:block/tags (if tag?
|
||||
:logseq.class/Tag
|
||||
:logseq.class/Page)
|
||||
:block/created-at now
|
||||
:block/updated-at now}
|
||||
page-tx (cond-> page-tx
|
||||
tag?
|
||||
(assoc :db/ident (db-class/create-user-class-ident-from-name db page-title)
|
||||
:logseq.property.class/extends :logseq.class/Root))]
|
||||
(recur more
|
||||
(assoc planned-pages page-name ref)
|
||||
(conj refs ref)
|
||||
(cond-> tag-refs tag? (conj ref))
|
||||
(conj page-txs page-tx)))))))))
|
||||
|
||||
(defn- with-content-ref-plans
|
||||
[db blocks now]
|
||||
(loop [[block & more] blocks
|
||||
planned-pages {}
|
||||
blocks' []
|
||||
page-txs []]
|
||||
(if (nil? block)
|
||||
{:blocks blocks'
|
||||
:page-txs page-txs}
|
||||
(let [{planned-pages' :planned-pages
|
||||
refs :refs
|
||||
tag-refs :tag-refs
|
||||
new-page-txs :page-txs
|
||||
db-title :db-title} (page-ref-plan db (:title block) planned-pages now)]
|
||||
(recur more
|
||||
planned-pages'
|
||||
(conj blocks' (assoc block
|
||||
:db-title db-title
|
||||
:content-ref-uuids (set (map :block/uuid refs))
|
||||
:tag-ref-uuids (set (map :block/uuid tag-refs))))
|
||||
(into page-txs new-page-txs))))))
|
||||
|
||||
(defn- page-root-blocks
|
||||
[page]
|
||||
(sort-by :block/order (:block/_parent page)))
|
||||
|
||||
(defn- page-blocks-by-uuid
|
||||
[db page]
|
||||
(->> (mapcat #(ldb/get-block-and-children db (:block/uuid %))
|
||||
(page-root-blocks page))
|
||||
(filter :block/uuid)
|
||||
(map (juxt :block/uuid identity))
|
||||
(into {})))
|
||||
|
||||
(defn- duplicate-marker?
|
||||
[blocks]
|
||||
(let [markers (keep :uuid blocks)]
|
||||
(not= (count markers) (count (set markers)))))
|
||||
|
||||
(defn- parsed-existing-markers-valid?
|
||||
[blocks page-block-by-uuid]
|
||||
(every? #(contains? page-block-by-uuid %) (keep :uuid blocks)))
|
||||
|
||||
(defn- parsed-parent-valid?
|
||||
[{:keys [parent-ref]} page-block-by-uuid new-uuid-by-index]
|
||||
(or (nil? parent-ref)
|
||||
(contains? page-block-by-uuid parent-ref)
|
||||
(contains? new-uuid-by-index parent-ref)))
|
||||
|
||||
(defn- new-block-tx
|
||||
[page-id block page-block-by-uuid new-uuid-by-index now]
|
||||
(let [block-uuid (or (:uuid block)
|
||||
(get new-uuid-by-index (:idx block)))
|
||||
parent-id (if-let [parent-ref (:parent-ref block)]
|
||||
(if-let [parent (:db/id (get page-block-by-uuid parent-ref))]
|
||||
parent
|
||||
[:block/uuid (get new-uuid-by-index parent-ref)])
|
||||
page-id)]
|
||||
{:block/uuid block-uuid
|
||||
:block/title (or (:db-title block) (:title block))
|
||||
:block/page page-id
|
||||
:block/parent parent-id
|
||||
:block/order (db-order/gen-key)
|
||||
:block/created-at now
|
||||
:block/updated-at now}))
|
||||
|
||||
(defn- top-level-delete-uuids
|
||||
[db delete-uuids]
|
||||
(let [delete-set (set delete-uuids)]
|
||||
(->> delete-uuids
|
||||
(remove (fn [block-uuid]
|
||||
(some (fn [ancestor]
|
||||
(and (not= block-uuid (:block/uuid ancestor))
|
||||
(contains? delete-set (:block/uuid ancestor))))
|
||||
(ldb/get-block-parents db block-uuid {:depth 100}))))
|
||||
vec)))
|
||||
|
||||
(defn- update-existing-block-tx
|
||||
[block parsed-block now]
|
||||
(let [title (or (:db-title parsed-block) (:title parsed-block))]
|
||||
(when (not= (:block/title block) title)
|
||||
{:db/id (:db/id block)
|
||||
:block/title title
|
||||
:block/updated-at now})))
|
||||
|
||||
(defn- title-content-ref-uuids
|
||||
[title]
|
||||
(if (string? title)
|
||||
(set (db-content/get-matched-ids title))
|
||||
#{}))
|
||||
|
||||
(defn- title-ref-uuids
|
||||
[db title]
|
||||
(->> (content-ref-targets title)
|
||||
(keep (fn [{:keys [title]}]
|
||||
(:block/uuid (ldb/get-page db title))))
|
||||
set))
|
||||
|
||||
(defn- title-tag-ref-uuids
|
||||
[db title]
|
||||
(->> (content-ref-targets title)
|
||||
(keep (fn [{:keys [title tag?]}]
|
||||
(when tag?
|
||||
(:block/uuid (ldb/get-page db title)))))
|
||||
set))
|
||||
|
||||
(defn- content-ref-add-txs
|
||||
[block-ref block]
|
||||
(map (fn [ref-uuid]
|
||||
[:db/add block-ref :block/refs [:block/uuid ref-uuid]])
|
||||
(:content-ref-uuids block)))
|
||||
|
||||
(defn- tag-ref-add-txs
|
||||
[block-ref block]
|
||||
(map (fn [tag-uuid]
|
||||
[:db/add block-ref :block/tags [:block/uuid tag-uuid]])
|
||||
(:tag-ref-uuids block)))
|
||||
|
||||
(defn- existing-block-content-ref-txs
|
||||
[db block parsed-block]
|
||||
(let [old-ref-uuids (set (concat (title-content-ref-uuids (:block/title block))
|
||||
(title-ref-uuids db (:block/title block))))
|
||||
new-ref-uuids (:content-ref-uuids parsed-block)
|
||||
block-id (:db/id block)]
|
||||
(concat
|
||||
(map (fn [ref-uuid]
|
||||
[:db/retract block-id :block/refs [:block/uuid ref-uuid]])
|
||||
(remove new-ref-uuids old-ref-uuids))
|
||||
(map (fn [ref-uuid]
|
||||
[:db/add block-id :block/refs [:block/uuid ref-uuid]])
|
||||
(remove old-ref-uuids new-ref-uuids)))))
|
||||
|
||||
(defn- existing-block-tag-ref-txs
|
||||
[db block parsed-block]
|
||||
(let [old-content-ref-uuids (set (concat (title-content-ref-uuids (:block/title block))
|
||||
(title-tag-ref-uuids db (:block/title block))))
|
||||
old-tag-uuids (->> (:block/tags block)
|
||||
(keep (fn [tag]
|
||||
(let [tag-uuid (:block/uuid tag)]
|
||||
(when (contains? old-content-ref-uuids tag-uuid)
|
||||
tag-uuid))))
|
||||
set)
|
||||
new-tag-uuids (:tag-ref-uuids parsed-block)
|
||||
block-id (:db/id block)]
|
||||
(concat
|
||||
(map (fn [tag-uuid]
|
||||
[:db/retract block-id :block/tags [:block/uuid tag-uuid]])
|
||||
(remove new-tag-uuids old-tag-uuids))
|
||||
(map (fn [tag-uuid]
|
||||
[:db/add block-id :block/tags [:block/uuid tag-uuid]])
|
||||
(remove old-tag-uuids new-tag-uuids)))))
|
||||
|
||||
(defn- import-tx-meta
|
||||
[relative-path outliner-ops]
|
||||
{:outliner-op :markdown-mirror/import-page
|
||||
:markdown-mirror/source :file
|
||||
:markdown-mirror/path relative-path
|
||||
:outliner-ops (vec outliner-ops)})
|
||||
|
||||
(defn- transact-import!
|
||||
[conn tx-data relative-path outliner-ops]
|
||||
(when (seq tx-data)
|
||||
(ldb/transact! conn tx-data (import-tx-meta relative-path outliner-ops))))
|
||||
|
||||
(defn- materialize-markers-content
|
||||
[db page]
|
||||
(let [block-lines
|
||||
(letfn [(render-block [block level]
|
||||
(let [indent (apply str (repeat (dec level) " "))
|
||||
children (sort-by :block/order (:block/_parent block))]
|
||||
(concat [(str "<!-- logseq:block " (:block/uuid block) " -->")
|
||||
(str indent "- " (:block/title block))]
|
||||
(mapcat #(render-block % (inc level)) children))))]
|
||||
(mapcat #(render-block % 1) (page-root-blocks page)))]
|
||||
(string/join "\n"
|
||||
(cons (str "<!-- logseq:page " (:block/uuid page) " -->")
|
||||
block-lines))))
|
||||
|
||||
(defn- block-marker-lines
|
||||
[db page]
|
||||
(->> (mapcat #(ldb/get-block-and-children db (:block/uuid %))
|
||||
(page-root-blocks page))
|
||||
(map (fn [block] {:uuid (:block/uuid block)}))))
|
||||
|
||||
(defn- add-markers-to-rendered-content
|
||||
[db page content]
|
||||
(let [markers (block-marker-lines db page)]
|
||||
(loop [[line & more] (string/split-lines (or content ""))
|
||||
[marker & more-markers] markers
|
||||
lines [(str "<!-- logseq:page " (:block/uuid page) " -->")]]
|
||||
(if (nil? line)
|
||||
(string/join "\n" lines)
|
||||
(if (re-matches markdown-block-re line)
|
||||
(recur more
|
||||
more-markers
|
||||
(cond-> lines
|
||||
marker
|
||||
(conj (str "<!-- logseq:block " (:uuid marker) " -->"))
|
||||
true
|
||||
(conj line)))
|
||||
(recur more
|
||||
(cons marker more-markers)
|
||||
(conj lines line)))))))
|
||||
|
||||
(defn- <materialize-markers!
|
||||
[repo db page {:keys [platform] :as _opts}]
|
||||
(when platform
|
||||
(when-let [relative-path (page-relative-path db page)]
|
||||
(<write-if-changed! platform
|
||||
repo
|
||||
(mirror-path repo relative-path)
|
||||
(materialize-markers-content db page)))))
|
||||
|
||||
(defn- import-new-file!
|
||||
[repo conn relative-path _content parsed opts]
|
||||
(let [db @conn
|
||||
page-plan (relative-path->new-page relative-path)]
|
||||
(cond
|
||||
(nil? page-plan)
|
||||
(p/resolved {:status :error
|
||||
:reason :unsupported-path})
|
||||
|
||||
(:page-uuid parsed)
|
||||
(p/resolved {:status :error
|
||||
:reason :new-file-has-page-marker})
|
||||
|
||||
(seq (keep :uuid (:blocks parsed)))
|
||||
(p/resolved {:status :error
|
||||
:reason :new-file-has-block-marker})
|
||||
|
||||
(existing-page-for-path db relative-path nil)
|
||||
(p/resolved {:status :error
|
||||
:reason :page-already-exists})
|
||||
|
||||
:else
|
||||
(let [now (common-util/time-ms)
|
||||
{parsed-blocks :blocks
|
||||
ref-page-txs :page-txs} (with-content-ref-plans db (:blocks parsed) now)
|
||||
page-uuid (:uuid page-plan)
|
||||
page-tx (cond-> {:block/title (:title page-plan)
|
||||
:block/name (common-util/page-name-sanity-lc (:title page-plan))
|
||||
:block/uuid page-uuid
|
||||
:block/tags :logseq.class/Page
|
||||
:block/created-at now
|
||||
:block/updated-at now}
|
||||
(= :journal (:type page-plan))
|
||||
(assoc :block/journal-day (:journal-day page-plan)
|
||||
:block/tags :logseq.class/Journal))
|
||||
new-uuid-by-index (->> parsed-blocks
|
||||
(map-indexed (fn [idx _] [idx (random-uuid)]))
|
||||
(into {}))
|
||||
block-txs (map-indexed
|
||||
(fn [_idx block]
|
||||
(new-block-tx [:block/uuid page-uuid] block {} new-uuid-by-index now))
|
||||
parsed-blocks)
|
||||
block-ref-txs (mapcat (fn [block]
|
||||
(content-ref-add-txs
|
||||
[:block/uuid (get new-uuid-by-index (:idx block))]
|
||||
block))
|
||||
parsed-blocks)
|
||||
block-tag-txs (mapcat (fn [block]
|
||||
(tag-ref-add-txs
|
||||
[:block/uuid (get new-uuid-by-index (:idx block))]
|
||||
block))
|
||||
parsed-blocks)
|
||||
tx-data (vec (concat [page-tx] ref-page-txs block-txs block-ref-txs block-tag-txs))
|
||||
page-options (cond-> {:redirect? false
|
||||
:uuid page-uuid}
|
||||
(= :journal (:type page-plan))
|
||||
(assoc :journal? true
|
||||
:journal-day (:journal-day page-plan)))
|
||||
outliner-ops [[:create-page [(:title page-plan) page-options]]
|
||||
[:insert-blocks [(vec block-txs) page-uuid {:sibling? false}]]]]
|
||||
(transact-import! conn tx-data relative-path outliner-ops)
|
||||
(p/let [_ (<materialize-markers! repo @conn (d/entity @conn [:block/uuid page-uuid]) opts)]
|
||||
{:status :imported
|
||||
:reason :new-file
|
||||
:page-uuid page-uuid})))))
|
||||
|
||||
(defn- import-existing-file!
|
||||
[repo conn relative-path parsed page opts]
|
||||
(let [db @conn
|
||||
page-block-by-uuid (page-blocks-by-uuid db page)]
|
||||
(cond
|
||||
(not= (:block/uuid page) (:page-uuid parsed))
|
||||
(p/resolved {:status :error
|
||||
:reason :page-marker-mismatch})
|
||||
|
||||
(duplicate-marker? (:blocks parsed))
|
||||
(p/resolved {:status :error
|
||||
:reason :duplicate-block-marker})
|
||||
|
||||
(not (parsed-existing-markers-valid? (:blocks parsed) page-block-by-uuid))
|
||||
(p/resolved {:status :error
|
||||
:reason :block-marker-outside-page})
|
||||
|
||||
:else
|
||||
(let [new-uuids (->> (:blocks parsed)
|
||||
(keep-indexed (fn [idx block]
|
||||
(when (nil? (:uuid block))
|
||||
[idx (random-uuid)])))
|
||||
(into {}))
|
||||
now (common-util/time-ms)
|
||||
{parsed-blocks :blocks
|
||||
ref-page-txs :page-txs} (with-content-ref-plans db (:blocks parsed) now)]
|
||||
(if-not (every? #(parsed-parent-valid? % page-block-by-uuid new-uuids)
|
||||
parsed-blocks)
|
||||
(p/resolved {:status :error
|
||||
:reason :unresolved-parent})
|
||||
(let [seen-markers (set (keep :uuid parsed-blocks))
|
||||
existing-uuids (set (keys page-block-by-uuid))
|
||||
delete-uuids (top-level-delete-uuids db (remove seen-markers existing-uuids))
|
||||
save-txs (keep (fn [block]
|
||||
(when-let [uuid (:uuid block)]
|
||||
(update-existing-block-tx (get page-block-by-uuid uuid) block now)))
|
||||
parsed-blocks)
|
||||
save-ref-txs (mapcat (fn [block]
|
||||
(when-let [uuid (:uuid block)]
|
||||
(existing-block-content-ref-txs
|
||||
db
|
||||
(get page-block-by-uuid uuid)
|
||||
block)))
|
||||
parsed-blocks)
|
||||
save-tag-txs (mapcat (fn [block]
|
||||
(when-let [uuid (:uuid block)]
|
||||
(existing-block-tag-ref-txs
|
||||
db
|
||||
(get page-block-by-uuid uuid)
|
||||
block)))
|
||||
parsed-blocks)
|
||||
new-blocks (keep-indexed (fn [idx block]
|
||||
(when (nil? (:uuid block))
|
||||
(new-block-tx (:db/id page)
|
||||
block
|
||||
page-block-by-uuid
|
||||
new-uuids
|
||||
now)))
|
||||
parsed-blocks)
|
||||
new-block-ref-txs (mapcat (fn [block]
|
||||
(when (nil? (:uuid block))
|
||||
(content-ref-add-txs
|
||||
[:block/uuid (get new-uuids (:idx block))]
|
||||
block)))
|
||||
parsed-blocks)
|
||||
new-block-tag-txs (mapcat (fn [block]
|
||||
(when (nil? (:uuid block))
|
||||
(tag-ref-add-txs
|
||||
[:block/uuid (get new-uuids (:idx block))]
|
||||
block)))
|
||||
parsed-blocks)
|
||||
delete-txs (mapcat (fn [block-uuid]
|
||||
[[:db/retractEntity (:db/id (get page-block-by-uuid block-uuid))]])
|
||||
delete-uuids)
|
||||
tx-data (vec (concat ref-page-txs
|
||||
save-txs
|
||||
save-ref-txs
|
||||
save-tag-txs
|
||||
new-blocks
|
||||
new-block-ref-txs
|
||||
new-block-tag-txs
|
||||
delete-txs))
|
||||
outliner-ops (cond-> []
|
||||
(seq save-txs)
|
||||
(conj [:save-block [(first save-txs) {}]])
|
||||
(seq new-blocks)
|
||||
(conj [:insert-blocks [(vec new-blocks) (:block/uuid page) {:sibling? false}]])
|
||||
(seq delete-uuids)
|
||||
(conj [:delete-blocks [(vec delete-uuids) {}]]))]
|
||||
(if (seq tx-data)
|
||||
(do
|
||||
(transact-import! conn tx-data relative-path outliner-ops)
|
||||
(p/let [_ (when (or (seq new-blocks) (seq delete-uuids))
|
||||
(<materialize-markers! repo @conn (d/entity @conn (:db/id page)) opts))]
|
||||
{:status :imported
|
||||
:count (count tx-data)}))
|
||||
(p/resolved {:status :skipped
|
||||
:reason :no-db-changes}))))))))
|
||||
|
||||
(defn <import-file-content!
|
||||
[repo conn relative-path content {:keys [platform] :as opts}]
|
||||
(let [parsed (parse-mirror-content content)
|
||||
page (existing-page-for-path @conn relative-path (:page-uuid parsed))]
|
||||
(if (:error parsed)
|
||||
(p/resolved {:status :error
|
||||
:reason (:error parsed)})
|
||||
(if (and platform (not (supported-runtime? platform)))
|
||||
(p/resolved {:status :skipped
|
||||
:reason :unsupported-runtime})
|
||||
(if page
|
||||
(import-existing-file! repo conn relative-path parsed page opts)
|
||||
(import-new-file! repo conn relative-path content parsed opts))))))
|
||||
|
||||
(defn <handle-file-event!
|
||||
[repo conn {:keys [type relative-path content]} {:keys [platform] :as opts}]
|
||||
(case type
|
||||
:deleted
|
||||
(p/resolved {:status :skipped
|
||||
:reason :ignored-delete-event})
|
||||
|
||||
:moved
|
||||
(p/resolved {:status :skipped
|
||||
:reason :ignored-move-event})
|
||||
|
||||
:changed
|
||||
(p/let [content* (if (some? content)
|
||||
content
|
||||
(when platform
|
||||
(<read-text platform (mirror-path repo relative-path))))]
|
||||
(if (some? content*)
|
||||
(<import-file-content! repo conn relative-path content* opts)
|
||||
{:status :skipped
|
||||
:reason :missing-file-content}))
|
||||
|
||||
(p/resolved {:status :skipped
|
||||
:reason :ignored-file-event})))
|
||||
|
||||
(defn stop-file-watcher!
|
||||
[repo]
|
||||
(when-let [watcher (get @*repo->file-watchers repo)]
|
||||
(when (.-close watcher)
|
||||
(.close watcher)))
|
||||
(swap! *repo->file-watchers dissoc repo)
|
||||
(swap! *repo->recent-writes dissoc repo)
|
||||
nil)
|
||||
|
||||
(defn- chokidar-watch!
|
||||
[watch-path opts]
|
||||
(let [chokidar (js/require "chokidar")]
|
||||
(.watch chokidar watch-path (clj->js opts))))
|
||||
|
||||
(defn- register-watch-handler!
|
||||
[watcher event handler]
|
||||
(.on ^js watcher event handler)
|
||||
watcher)
|
||||
|
||||
(defn <start-file-watcher!
|
||||
[repo conn {:keys [platform ignored-recent-write-ms] :as opts}]
|
||||
(let [platform* (or platform (platform/current))]
|
||||
(cond
|
||||
(not (supported-runtime? platform*))
|
||||
(p/resolved {:status :skipped
|
||||
:reason :unsupported-runtime})
|
||||
|
||||
(nil? conn)
|
||||
(p/resolved {:status :skipped
|
||||
:reason :missing-conn})
|
||||
|
||||
:else
|
||||
(let [watch-root (watch-root-path platform* repo)
|
||||
watch! (or (:chokidar-watch! opts) chokidar-watch!)
|
||||
watcher (watch! watch-root {:ignore-initial true
|
||||
:await-write-finish {:stability-threshold 200
|
||||
:poll-interval 50}})]
|
||||
(stop-file-watcher! repo)
|
||||
(swap! *repo->file-watchers assoc repo watcher)
|
||||
(letfn [(handle-result [relative-path event promise]
|
||||
(-> promise
|
||||
(p/catch (fn [error]
|
||||
(log/error :markdown-mirror/import-file-event-failed
|
||||
{:repo repo
|
||||
:relative-path relative-path
|
||||
:event event
|
||||
:error error})))))
|
||||
(handle-path! [event path]
|
||||
(when-let [relative-path (watcher-event-relative-path repo watch-root path)]
|
||||
(let [storage-path (mirror-path repo relative-path)
|
||||
ttl-ms (or ignored-recent-write-ms 1000)]
|
||||
(if (= :changed event)
|
||||
(handle-result
|
||||
relative-path
|
||||
event
|
||||
(p/let [content (<read-text platform* storage-path)]
|
||||
(if (= content (recent-written-content repo storage-path ttl-ms))
|
||||
{:status :skipped
|
||||
:reason :ignored-self-write}
|
||||
(<handle-file-event! repo conn {:type event
|
||||
:relative-path relative-path
|
||||
:content content}
|
||||
{:platform platform*}))))
|
||||
(handle-result
|
||||
relative-path
|
||||
event
|
||||
(<handle-file-event! repo conn {:type event
|
||||
:relative-path relative-path}
|
||||
{:platform platform*}))))))]
|
||||
(register-watch-handler! watcher "add" #(handle-path! :changed %))
|
||||
(register-watch-handler! watcher "change" #(handle-path! :changed %))
|
||||
(register-watch-handler! watcher "unlink" #(handle-path! :deleted %))
|
||||
(register-watch-handler! watcher "unlinkDir" #(handle-path! :deleted %))
|
||||
(register-watch-handler! watcher "error"
|
||||
#(log/error :markdown-mirror/watch-error
|
||||
{:repo repo
|
||||
:error %})))
|
||||
(p/resolved {:status :watching
|
||||
:path watch-root})))))
|
||||
|
||||
(defn- page-id-for-entity
|
||||
[db eid]
|
||||
(when-let [entity (d/entity db eid)]
|
||||
@@ -168,12 +949,15 @@
|
||||
|
||||
(defn- render-page-content
|
||||
[db page options]
|
||||
(common-file/block->content
|
||||
(add-markers-to-rendered-content
|
||||
db
|
||||
(:block/uuid page)
|
||||
{:include-page-properties? true}
|
||||
{:export-bullet-indentation (or (:export-bullet-indentation options) " ")
|
||||
:date-formatter (:date-formatter options)}))
|
||||
page
|
||||
(common-file/block->content
|
||||
db
|
||||
(:block/uuid page)
|
||||
{:include-page-properties? true}
|
||||
{:export-bullet-indentation (or (:export-bullet-indentation options) " ")
|
||||
:date-formatter (:date-formatter options)})))
|
||||
|
||||
(defn- mirrorable-page?
|
||||
[page]
|
||||
@@ -195,13 +979,14 @@
|
||||
(str (:block/uuid page))]))))
|
||||
|
||||
(defn- <write-if-changed!
|
||||
[platform* path content]
|
||||
[platform* repo path content]
|
||||
(p/let [current (<read-text platform* path)]
|
||||
(if (= current content)
|
||||
{:status :skipped
|
||||
:reason :unchanged
|
||||
:path path}
|
||||
(p/let [_ (<write-text-atomic! platform* path content)]
|
||||
(p/let [_ (<write-text-atomic! platform* path content)
|
||||
_ (mark-recent-write! repo path content)]
|
||||
{:status :written
|
||||
:path path}))))
|
||||
|
||||
@@ -242,7 +1027,7 @@
|
||||
(if-let [relative-path (page-relative-path db page opts)]
|
||||
(let [path (mirror-path repo relative-path)
|
||||
content (render-page-content db page opts)]
|
||||
(<write-if-changed! platform* path content))
|
||||
(<write-if-changed! platform* repo path content))
|
||||
(p/resolved (invalid-file-name-result repo page))))
|
||||
(p/resolved {:status :skipped
|
||||
:reason :missing-page
|
||||
|
||||
@@ -448,6 +448,8 @@
|
||||
:db-exists? (fn [graph] (db-exists? data-dir graph))
|
||||
:resolve-db-path (fn [_repo pool path]
|
||||
(pool-path pool path))
|
||||
:resolve-text-path (fn [path]
|
||||
(path-under-data-dir data-dir path))
|
||||
:export-file export-file
|
||||
:import-db (fn [pool path data] (import-db write-guard-fn pool path data))
|
||||
:remove-vfs! (fn [pool] (remove-vfs! pool))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
(ns frontend.worker.markdown-mirror-test
|
||||
(:require [cljs.test :refer [async deftest is testing]]
|
||||
(:require [clojure.string :as string]
|
||||
[cljs.test :refer [async deftest is testing]]
|
||||
[datascript.core :as d]
|
||||
[frontend.worker.db-listener :as db-listener]
|
||||
[frontend.worker.markdown-mirror :as markdown-mirror]
|
||||
@@ -19,6 +20,8 @@
|
||||
{:platform {:env env
|
||||
:storage {:read-text! (fn [path]
|
||||
(p/resolved (get @files path)))
|
||||
:resolve-text-path (fn [path]
|
||||
(str "/tmp/logseq/" path))
|
||||
:write-text-atomic! (fn [path content]
|
||||
(swap! writes conj [path content])
|
||||
(swap! files assoc path content)
|
||||
@@ -38,6 +41,22 @@
|
||||
(defn- first-block [page]
|
||||
(-> page :block/_page first))
|
||||
|
||||
(defn- block-by-title [db title]
|
||||
(->> (d/datoms db :avet :block/title title)
|
||||
(map #(d/entity db (:e %)))
|
||||
(filter :block/page)
|
||||
first))
|
||||
|
||||
(defn- block-title-includes?
|
||||
[db block-uuid s]
|
||||
(string/includes? (:block/title (d/entity db [:block/uuid block-uuid])) s))
|
||||
|
||||
(defn- page-marker [uuid]
|
||||
(str "<!-- logseq:page " uuid " -->"))
|
||||
|
||||
(defn- block-marker [uuid]
|
||||
(str "<!-- logseq:block " uuid " -->"))
|
||||
|
||||
(defn- <mirror-repo!
|
||||
[& args]
|
||||
(if-let [f (resolve 'frontend.worker.markdown-mirror/<mirror-repo!)]
|
||||
@@ -87,9 +106,13 @@
|
||||
(deftest page-references-remain-wiki-links-test
|
||||
(async done
|
||||
(let [{:keys [platform files]} (fake-platform)
|
||||
page-uuid #uuid "11111111-1111-4111-8111-111111111113"
|
||||
block-uuid #uuid "11111111-1111-4111-8111-111111111114"
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks [{:page {:block/title "Source"}
|
||||
:blocks [{:block/title "See [[Foo]]"}]}
|
||||
{:pages-and-blocks [{:page {:block/title "Source"
|
||||
:block/uuid page-uuid}
|
||||
:blocks [{:block/title "See [[Foo]]"
|
||||
:block/uuid block-uuid}]}
|
||||
{:page {:block/title "Foo"}
|
||||
:blocks [{:block/title "target"}]}
|
||||
{:page {:block/title "Foo"}
|
||||
@@ -97,7 +120,9 @@
|
||||
page (db-test/find-page-by-title @conn "Source")]
|
||||
(-> (markdown-mirror/<mirror-page! test-repo @conn (:db/id page) {:platform platform})
|
||||
(p/then (fn [_]
|
||||
(is (= "- See [[Foo]]"
|
||||
(is (= (str (page-marker page-uuid) "\n"
|
||||
(block-marker block-uuid) "\n"
|
||||
"- See [[Foo]]")
|
||||
(get @files (page-path "pages/Source.md"))))))
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
@@ -117,30 +142,43 @@
|
||||
(async done
|
||||
(let [{:keys [platform files writes]} (fake-platform)
|
||||
page-uuid #uuid "33333333-3333-4333-8333-333333333333"
|
||||
block-uuid-1 #uuid "33333333-3333-4333-8333-333333333334"
|
||||
block-uuid-2 #uuid "33333333-3333-4333-8333-333333333335"
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks [{:page {:block/title "Page A"
|
||||
:block/uuid page-uuid}
|
||||
:blocks [{:block/title "hello"}
|
||||
{:block/title "world"}]}]})
|
||||
:blocks [{:block/title "hello"
|
||||
:block/uuid block-uuid-1}
|
||||
{:block/title "world"
|
||||
:block/uuid block-uuid-2}]}]})
|
||||
page (db-test/find-page-by-title @conn "Page A")]
|
||||
(-> (markdown-mirror/<mirror-page! test-repo @conn (:db/id page) {:platform platform})
|
||||
(p/then (fn [_]
|
||||
(let [path (page-path "pages/Page A.md")]
|
||||
(is (= "- hello\n- world" (get @files path)))
|
||||
(is (= [[path "- hello\n- world"]] @writes)))))
|
||||
(let [path (page-path "pages/Page A.md")
|
||||
content (str (page-marker page-uuid) "\n"
|
||||
(block-marker block-uuid-1) "\n"
|
||||
"- hello\n"
|
||||
(block-marker block-uuid-2) "\n"
|
||||
"- world")]
|
||||
(is (= content (get @files path)))
|
||||
(is (= [[path content]] @writes)))))
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest page-mirror-exports-property-values-test
|
||||
(async done
|
||||
(let [{:keys [platform files]} (fake-platform)
|
||||
page-uuid #uuid "22222222-2222-4222-8222-222222222221"
|
||||
block-uuid #uuid "22222222-2222-4222-8222-222222222222"
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:properties {:user.property/reproducible-steps {:logseq.property/type :default}
|
||||
:user.property/rating {:logseq.property/type :number}}
|
||||
:pages-and-blocks [{:page {:block/title "Issue"
|
||||
:block/uuid page-uuid
|
||||
:build/properties {:user.property/reproducible-steps "Open settings"
|
||||
:logseq.property/heading 1}}
|
||||
:blocks [{:block/title "TODO body"
|
||||
:block/uuid block-uuid
|
||||
:build/properties {:logseq.property/status :logseq.property/status.todo
|
||||
:user.property/reproducible-steps "Click mirror"
|
||||
:user.property/rating 5
|
||||
@@ -149,7 +187,13 @@
|
||||
(-> (markdown-mirror/<mirror-page! test-repo @conn (:db/id page) {:platform platform})
|
||||
(p/then (fn [_]
|
||||
(let [content (get @files (page-path "pages/Issue.md"))]
|
||||
(is (= "reproducible-steps:: Open settings\n- TODO body\n Status:: Todo\n reproducible-steps:: Click mirror\n rating:: 5"
|
||||
(is (= (str (page-marker page-uuid) "\n"
|
||||
"reproducible-steps:: Open settings\n"
|
||||
(block-marker block-uuid) "\n"
|
||||
"- TODO body\n"
|
||||
" Status:: Todo\n"
|
||||
" reproducible-steps:: Click mirror\n"
|
||||
" rating:: 5")
|
||||
content)))))
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
@@ -157,20 +201,29 @@
|
||||
(deftest page-mirror-exports-page-property-values-test
|
||||
(async done
|
||||
(let [{:keys [platform files]} (fake-platform)
|
||||
page-uuid #uuid "22222222-2222-4222-8222-222222222223"
|
||||
block-uuid #uuid "22222222-2222-4222-8222-222222222224"
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:properties {:user.property/p1 {:logseq.property/type :default}
|
||||
:user.property/p2 {:logseq.property/type :number}
|
||||
:user.property/p3 {:logseq.property/type :default}}
|
||||
:pages-and-blocks [{:page {:block/title "Page Props"
|
||||
:block/uuid page-uuid
|
||||
:build/properties {:user.property/p1 "hello"
|
||||
:user.property/p2 1
|
||||
:user.property/p3 "Author 1"}}
|
||||
:blocks [{:block/title "body"}]}]})
|
||||
:blocks [{:block/title "body"
|
||||
:block/uuid block-uuid}]}]})
|
||||
page (db-test/find-page-by-title @conn "Page Props")]
|
||||
(-> (markdown-mirror/<mirror-page! test-repo @conn (:db/id page) {:platform platform})
|
||||
(p/then (fn [_]
|
||||
(let [content (get @files (page-path "pages/Page Props.md"))]
|
||||
(is (= "p1:: hello\np2:: 1\np3:: Author 1\n- body"
|
||||
(is (= (str (page-marker page-uuid) "\n"
|
||||
"p1:: hello\n"
|
||||
"p2:: 1\n"
|
||||
"p3:: Author 1\n"
|
||||
(block-marker block-uuid) "\n"
|
||||
"- body")
|
||||
content)))))
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
@@ -178,16 +231,20 @@
|
||||
(deftest journal-mirror-exports-page-and-block-property-values-test
|
||||
(async done
|
||||
(let [{:keys [platform files]} (fake-platform)
|
||||
page-uuid #uuid "44444444-4444-4444-8444-444444444441"
|
||||
block-uuid #uuid "44444444-4444-4444-8444-444444444442"
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:properties {:user.property/p1 {:logseq.property/type :default}
|
||||
:user.property/p2 {:logseq.property/type :number}
|
||||
:user.property/p3 {:logseq.property/type :default}}
|
||||
:pages-and-blocks [{:page {:block/title "May 5th, 2026"
|
||||
:block/name "may 5th, 2026"
|
||||
:block/uuid page-uuid
|
||||
:block/journal-day 20260505
|
||||
:block/tags #{:logseq.class/Journal}
|
||||
:build/properties {:user.property/p1 "hey"}}
|
||||
:blocks [{:block/title "TODO hello great test"
|
||||
:block/uuid block-uuid
|
||||
:build/properties {:logseq.property/status :logseq.property/status.todo
|
||||
:user.property/p1 "hello"
|
||||
:user.property/p2 1
|
||||
@@ -196,7 +253,14 @@
|
||||
(-> (markdown-mirror/<mirror-page! test-repo @conn (:db/id journal) {:platform platform})
|
||||
(p/then (fn [_]
|
||||
(let [content (get @files (page-path "journals/2026_05_05.md"))]
|
||||
(is (= "p1:: hey\n- TODO hello great test\n Status:: Todo\n p1:: hello\n p2:: 1\n p3:: Author 1"
|
||||
(is (= (str (page-marker page-uuid) "\n"
|
||||
"p1:: hey\n"
|
||||
(block-marker block-uuid) "\n"
|
||||
"- TODO hello great test\n"
|
||||
" Status:: Todo\n"
|
||||
" p1:: hello\n"
|
||||
" p2:: 1\n"
|
||||
" p3:: Author 1")
|
||||
content)))))
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
@@ -204,21 +268,33 @@
|
||||
(deftest full-regeneration-writes-existing-non-built-in-non-property-pages-test
|
||||
(async done
|
||||
(let [{:keys [platform files]} (fake-platform)
|
||||
page-uuid #uuid "55555555-5555-4555-8555-555555555551"
|
||||
page-block-uuid #uuid "55555555-5555-4555-8555-555555555552"
|
||||
journal-uuid #uuid "55555555-5555-4555-8555-555555555553"
|
||||
journal-block-uuid #uuid "55555555-5555-4555-8555-555555555554"
|
||||
class-uuid #uuid "55555555-5555-4555-8555-555555555555"
|
||||
class-block-uuid #uuid "55555555-5555-4555-8555-555555555556"
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:properties {:rating {:logseq.property/type :default}}
|
||||
:pages-and-blocks [{:page {:block/title "Page A"}
|
||||
:blocks [{:block/title "alpha"}]}
|
||||
:pages-and-blocks [{:page {:block/title "Page A"
|
||||
:block/uuid page-uuid}
|
||||
:blocks [{:block/title "alpha"
|
||||
:block/uuid page-block-uuid}]}
|
||||
{:page {:block/title "Journal"
|
||||
:block/uuid journal-uuid
|
||||
:block/journal-day 20240508
|
||||
:block/tags #{:logseq.class/Journal}}
|
||||
:blocks [{:block/title "journal"}]}
|
||||
:blocks [{:block/title "journal"
|
||||
:block/uuid journal-block-uuid}]}
|
||||
{:page {:block/title "Built In"
|
||||
:build/properties {:logseq.property/built-in? true}}
|
||||
:blocks [{:block/title "system"}]}
|
||||
{:page {:block/title "Project"
|
||||
:block/uuid class-uuid
|
||||
:block/tags #{:logseq.class/Tag}
|
||||
:db/ident :user.class/Project}
|
||||
:blocks [{:block/title "class"}]}
|
||||
:blocks [{:block/title "class"
|
||||
:block/uuid class-block-uuid}]}
|
||||
{:page {:block/title "rating"
|
||||
:block/tags #{:logseq.class/Property}
|
||||
:db/ident :user.property/rating}
|
||||
@@ -226,11 +302,17 @@
|
||||
(-> (<mirror-repo! test-repo @conn {:platform platform})
|
||||
(p/then (fn [result]
|
||||
(is (not= ::missing-mirror-repo-fn result))
|
||||
(is (= "- alpha"
|
||||
(is (= (str (page-marker page-uuid) "\n"
|
||||
(block-marker page-block-uuid) "\n"
|
||||
"- alpha")
|
||||
(get @files (page-path "pages/Page A.md"))))
|
||||
(is (= "- journal"
|
||||
(is (= (str (page-marker journal-uuid) "\n"
|
||||
(block-marker journal-block-uuid) "\n"
|
||||
"- journal")
|
||||
(get @files (page-path "journals/2024_05_08.md"))))
|
||||
(is (= "- class"
|
||||
(is (= (str (page-marker class-uuid) "\n"
|
||||
(block-marker class-block-uuid) "\n"
|
||||
"- class")
|
||||
(get @files (page-path "pages/Project.md"))))
|
||||
(is (nil? (get @files (page-path "pages/Built In.md"))))
|
||||
(is (nil? (get @files (page-path "pages/rating.md"))))))
|
||||
@@ -242,14 +324,18 @@
|
||||
(let [{:keys [platform files]} (fake-platform {:runtime :browser
|
||||
:owner-source :electron})
|
||||
page-uuid #uuid "88888888-8888-4888-8888-888888888888"
|
||||
block-uuid #uuid "88888888-8888-4888-8888-888888888889"
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks [{:page {:block/title "Page A"
|
||||
:block/uuid page-uuid}
|
||||
:blocks [{:block/title "desktop"}]}]})
|
||||
:blocks [{:block/title "desktop"
|
||||
:block/uuid block-uuid}]}]})
|
||||
page (db-test/find-page-by-title @conn "Page A")]
|
||||
(-> (markdown-mirror/<mirror-page! test-repo @conn (:db/id page) {:platform platform})
|
||||
(p/then (fn [_]
|
||||
(is (= "- desktop"
|
||||
(is (= (str (page-marker page-uuid) "\n"
|
||||
(block-marker block-uuid) "\n"
|
||||
"- desktop")
|
||||
(get @files (page-path "pages/Page A.md"))))))
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
@@ -273,17 +359,49 @@
|
||||
(deftest enabled-electron-edit-writes-journal-mirror-test
|
||||
(async done
|
||||
(let [{:keys [platform files]} (fake-platform)
|
||||
page-uuid #uuid "77777777-7777-4777-8777-777777777771"
|
||||
block-uuid #uuid "77777777-7777-4777-8777-777777777772"
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks [{:page {:build/journal 20240506}
|
||||
:blocks [{:block/title "journal item"}]}]})
|
||||
{:pages-and-blocks [{:page {:build/journal 20240506
|
||||
:block/uuid page-uuid}
|
||||
:blocks [{:block/title "journal item"
|
||||
:block/uuid block-uuid}]}]})
|
||||
journal (db-test/find-journal-by-journal-day @conn 20240506)]
|
||||
(-> (markdown-mirror/<mirror-page! test-repo @conn (:db/id journal) {:platform platform})
|
||||
(p/then (fn [_]
|
||||
(is (= "- journal item"
|
||||
(is (= (str (page-marker page-uuid) "\n"
|
||||
(block-marker block-uuid) "\n"
|
||||
"- journal item")
|
||||
(get @files (page-path "journals/2024_05_06.md"))))))
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest generated-journal-mirror-file-imports-back-into-db-test
|
||||
(async done
|
||||
(let [{:keys [platform files]} (fake-platform)
|
||||
page-uuid #uuid "77777777-7777-4777-8777-777777777773"
|
||||
block-uuid #uuid "77777777-7777-4777-8777-777777777774"
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks [{:page {:build/journal 20240507
|
||||
:block/uuid page-uuid}
|
||||
:blocks [{:block/title "before"
|
||||
:block/uuid block-uuid}]}]})
|
||||
journal (db-test/find-journal-by-journal-day @conn 20240507)
|
||||
relative-path "journals/2024_05_07.md"
|
||||
storage-path (page-path relative-path)]
|
||||
(-> (p/let [_ (markdown-mirror/<mirror-page! test-repo @conn (:db/id journal) {:platform platform})
|
||||
generated (get @files storage-path)
|
||||
_ (swap! files assoc storage-path (string/replace generated "- before" "- after"))
|
||||
result (markdown-mirror/<handle-file-event! test-repo conn {:type :changed
|
||||
:relative-path relative-path}
|
||||
{:platform platform})]
|
||||
(is (string/includes? generated (page-marker page-uuid)))
|
||||
(is (string/includes? generated (block-marker block-uuid)))
|
||||
(is (= :imported (:status result)))
|
||||
(is (= "after" (:block/title (d/entity @conn [:block/uuid block-uuid])))))
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest disabled-setting-does-not-write-mirror-test
|
||||
(async done
|
||||
(let [{:keys [platform writes]} (fake-platform)
|
||||
@@ -348,10 +466,12 @@
|
||||
(async done
|
||||
(let [{:keys [platform writes]} (fake-platform)
|
||||
page-uuid #uuid "44444444-4444-4444-8444-444444444444"
|
||||
block-uuid #uuid "44444444-4444-4444-8444-444444444445"
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks [{:page {:block/title "Page A"
|
||||
:block/uuid page-uuid}
|
||||
:blocks [{:block/title "before"}]}]})
|
||||
:blocks [{:block/title "before"
|
||||
:block/uuid block-uuid}]}]})
|
||||
page (db-test/find-page-by-title @conn "Page A")
|
||||
block (first-block page)
|
||||
tx-report-1 (d/with @conn [{:db/id (:db/id block)
|
||||
@@ -365,7 +485,10 @@
|
||||
_ (markdown-mirror/<handle-tx-report! test-repo conn tx-report-2 {:platform platform
|
||||
:defer? true})
|
||||
_ (markdown-mirror/<flush-repo! test-repo {:platform platform})]
|
||||
(is (= [[(page-path "pages/Page A.md") "- latest"]]
|
||||
(is (= [[(page-path "pages/Page A.md")
|
||||
(str (page-marker page-uuid) "\n"
|
||||
(block-marker block-uuid) "\n"
|
||||
"- latest")]]
|
||||
@writes)))
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
@@ -374,10 +497,12 @@
|
||||
(async done
|
||||
(let [{:keys [platform files deletes]} (fake-platform)
|
||||
page-uuid #uuid "55555555-5555-4555-8555-555555555555"
|
||||
block-uuid #uuid "55555555-5555-4555-8555-555555555557"
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks [{:page {:block/title "Old Name"
|
||||
:block/uuid page-uuid}
|
||||
:blocks [{:block/title "body"}]}]})
|
||||
:blocks [{:block/title "body"
|
||||
:block/uuid block-uuid}]}]})
|
||||
page (db-test/find-page-by-title @conn "Old Name")
|
||||
old-path (page-path "pages/Old Name.md")
|
||||
_ (swap! files assoc old-path "- body")
|
||||
@@ -389,7 +514,9 @@
|
||||
(-> (markdown-mirror/<handle-tx-report! test-repo conn tx-report {:platform platform})
|
||||
(p/then (fn [_]
|
||||
(is (= [old-path] @deletes))
|
||||
(is (= "- body"
|
||||
(is (= (str (page-marker page-uuid) "\n"
|
||||
(block-marker block-uuid) "\n"
|
||||
"- body")
|
||||
(get @files (page-path "pages/New Name.md"))))
|
||||
(is (nil? (get @files old-path)))))
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
@@ -444,21 +571,26 @@
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest unchanged-content-skips-write-test
|
||||
(deftest unchanged-generated-content-skips-write-test
|
||||
(async done
|
||||
(let [{:keys [platform files writes]} (fake-platform)
|
||||
page-uuid #uuid "77777777-7777-4777-8777-777777777777"
|
||||
block-uuid #uuid "77777777-7777-4777-8777-777777777778"
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks [{:page {:block/title "Page A"
|
||||
:block/uuid page-uuid}
|
||||
:blocks [{:block/title "same"}]}]})
|
||||
:blocks [{:block/title "same"
|
||||
:block/uuid block-uuid}]}]})
|
||||
page (db-test/find-page-by-title @conn "Page A")
|
||||
path (page-path "pages/Page A.md")
|
||||
_ (swap! files assoc path "- same")]
|
||||
content (str (page-marker page-uuid) "\n"
|
||||
(block-marker block-uuid) "\n"
|
||||
"- same")
|
||||
_ (swap! files assoc path content)]
|
||||
(-> (markdown-mirror/<mirror-page! test-repo @conn (:db/id page) {:platform platform})
|
||||
(p/then (fn [_]
|
||||
(is (empty? @writes))
|
||||
(is (= "- same" (get @files path)))))
|
||||
(is (= content (get @files path)))))
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
@@ -503,3 +635,389 @@
|
||||
(is (empty? @writes))))
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest two-way-file-edit-transacts-local-db-test
|
||||
(async done
|
||||
(let [page-uuid #uuid "99999999-9999-4999-8999-999999999999"
|
||||
block-uuid #uuid "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks [{:page {:block/title "Import Page"
|
||||
:block/uuid page-uuid}
|
||||
:blocks [{:block/title "before"
|
||||
:block/uuid block-uuid}]}]})
|
||||
relative-path "pages/Import Page.md"
|
||||
content (str (page-marker page-uuid) "\n"
|
||||
(block-marker block-uuid) "\n"
|
||||
"- after")]
|
||||
(-> (markdown-mirror/<import-file-content! test-repo conn relative-path content {})
|
||||
(p/then (fn [result]
|
||||
(is (= :imported (:status result)))
|
||||
(is (= "after" (:block/title (d/entity @conn [:block/uuid block-uuid]))))))
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest two-way-file-edit-can-change-generated-block-content-immediately-test
|
||||
(async done
|
||||
(let [{:keys [platform files]} (fake-platform)
|
||||
page-uuid #uuid "99999999-9999-4999-8999-999999999991"
|
||||
block-uuid #uuid "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1"
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks [{:page {:block/title "Immediate Edit"
|
||||
:block/uuid page-uuid}
|
||||
:blocks [{:block/title "before"
|
||||
:block/uuid block-uuid}]}]})
|
||||
page (db-test/find-page-by-title @conn "Immediate Edit")
|
||||
relative-path "pages/Immediate Edit.md"
|
||||
storage-path (page-path relative-path)
|
||||
handlers (atom {})
|
||||
watcher #js {}
|
||||
_ (set! (.-on watcher)
|
||||
(fn [event handler]
|
||||
(swap! handlers assoc event handler)
|
||||
watcher))
|
||||
_ (set! (.-close watcher) (fn [] nil))]
|
||||
(-> (p/let [_ (markdown-mirror/<start-file-watcher! test-repo conn {:platform platform
|
||||
:chokidar-watch! (fn [_path _opts] watcher)
|
||||
:ignored-recent-write-ms 60000})
|
||||
_ (markdown-mirror/<mirror-page! test-repo @conn (:db/id page) {:platform platform})
|
||||
generated (get @files storage-path)
|
||||
_ (swap! files assoc storage-path (string/replace generated "- before" "- after"))
|
||||
result ((get @handlers "change") (str "/tmp/logseq/" storage-path))
|
||||
_ (markdown-mirror/stop-file-watcher! test-repo)]
|
||||
(is (= :imported (:status result)))
|
||||
(is (= "after" (:block/title (d/entity @conn [:block/uuid block-uuid])))))
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest two-way-existing-block-page-ref-edit-creates-page-test
|
||||
(async done
|
||||
(let [page-uuid #uuid "99999999-9999-4999-8999-999999999990"
|
||||
block-uuid #uuid "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa2"
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks [{:page {:block/title "Ref Edit"
|
||||
:block/uuid page-uuid}
|
||||
:blocks [{:block/title "before"
|
||||
:block/uuid block-uuid}]}]})
|
||||
relative-path "pages/Ref Edit.md"
|
||||
content (str (page-marker page-uuid) "\n"
|
||||
(block-marker block-uuid) "\n"
|
||||
"- [[foo]] test")]
|
||||
(-> (markdown-mirror/<import-file-content! test-repo conn relative-path content {})
|
||||
(p/then (fn [result]
|
||||
(let [foo (db-test/find-page-by-title @conn "foo")
|
||||
block (d/entity @conn [:block/uuid block-uuid])]
|
||||
(is (= :imported (:status result)))
|
||||
(is (some? foo))
|
||||
(is (block-title-includes? @conn block-uuid "[[foo]]"))
|
||||
(is (= #{(:db/id foo)} (set (map :db/id (:block/refs block))))))))
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest two-way-existing-block-hashtag-edit-creates-tag-test
|
||||
(async done
|
||||
(let [page-uuid #uuid "99999999-9999-4999-8999-999999999988"
|
||||
block-uuid #uuid "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa4"
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks [{:page {:block/title "Tag Edit"
|
||||
:block/uuid page-uuid}
|
||||
:blocks [{:block/title "before"
|
||||
:block/uuid block-uuid}]}]})
|
||||
content (str (page-marker page-uuid) "\n"
|
||||
(block-marker block-uuid) "\n"
|
||||
"- object 1 #tag1")]
|
||||
(-> (markdown-mirror/<import-file-content! test-repo conn "pages/Tag Edit.md" content {})
|
||||
(p/then (fn [result]
|
||||
(let [tag (db-test/find-page-by-title @conn "tag1")
|
||||
block (d/entity @conn [:block/uuid block-uuid])]
|
||||
(is (= :imported (:status result)))
|
||||
(is (some? tag))
|
||||
(is (some #(= :logseq.class/Tag (:db/ident %)) (:block/tags tag)))
|
||||
(is (block-title-includes? @conn block-uuid "#tag1"))
|
||||
(is (= #{(:db/id tag)} (set (map :db/id (:block/tags block)))))
|
||||
(is (= #{(:db/id tag)} (set (map :db/id (:block/refs block))))))))
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest two-way-existing-block-preserves-continuation-markdown-test
|
||||
(async done
|
||||
(let [page-uuid #uuid "99999999-9999-4999-8999-999999999987"
|
||||
block-uuid #uuid "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa5"
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks [{:page {:block/title "Continuation Edit"
|
||||
:block/uuid page-uuid}
|
||||
:blocks [{:block/title "before"
|
||||
:block/uuid block-uuid}]}]})
|
||||
content (str (page-marker page-uuid) "\n"
|
||||
(block-marker block-uuid) "\n"
|
||||
"- intro\n"
|
||||
" > quoted\n"
|
||||
" ```clojure\n"
|
||||
" - not a child\n"
|
||||
" ```")]
|
||||
(-> (markdown-mirror/<import-file-content! test-repo conn "pages/Continuation Edit.md" content {})
|
||||
(p/then (fn [result]
|
||||
(let [page (db-test/find-page-by-title @conn "Continuation Edit")
|
||||
page-blocks (map #(d/entity @conn (:e %))
|
||||
(d/datoms @conn :avet :block/page (:db/id page)))]
|
||||
(is (= :imported (:status result)))
|
||||
(is (= "intro\n> quoted\n```clojure\n- not a child\n```"
|
||||
(:block/title (d/entity @conn [:block/uuid block-uuid]))))
|
||||
(is (= 1 (count page-blocks))))))
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest two-way-inserted-block-preserves-continuation-markdown-test
|
||||
(async done
|
||||
(let [page-uuid #uuid "99999999-9999-4999-8999-999999999986"
|
||||
block-uuid #uuid "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa6"
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks [{:page {:block/title "Continuation Insert"
|
||||
:block/uuid page-uuid}
|
||||
:blocks [{:block/title "existing"
|
||||
:block/uuid block-uuid}]}]})
|
||||
content (str (page-marker page-uuid) "\n"
|
||||
(block-marker block-uuid) "\n"
|
||||
"- existing\n"
|
||||
"- inserted\n"
|
||||
" > quote")]
|
||||
(-> (markdown-mirror/<import-file-content! test-repo conn "pages/Continuation Insert.md" content {})
|
||||
(p/then (fn [result]
|
||||
(is (= :imported (:status result)))
|
||||
(is (some? (block-by-title @conn "inserted\n> quote")))))
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest two-way-top-level-markdown-without-block-is-rejected-test
|
||||
(async done
|
||||
(let [page-uuid #uuid "99999999-9999-4999-8999-999999999985"
|
||||
block-uuid #uuid "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa7"
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks [{:page {:block/title "Top Level Markdown"
|
||||
:block/uuid page-uuid}
|
||||
:blocks [{:block/title "existing"
|
||||
:block/uuid block-uuid}]}]})
|
||||
content (str (page-marker page-uuid) "\n"
|
||||
"> top-level quote")]
|
||||
(-> (markdown-mirror/<import-file-content! test-repo conn "pages/Top Level Markdown.md" content {})
|
||||
(p/then (fn [result]
|
||||
(is (= :error (:status result)))
|
||||
(is (= :unsupported-top-level-markdown (:reason result)))
|
||||
(is (= "existing" (:block/title (d/entity @conn [:block/uuid block-uuid]))))))
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest two-way-property-edits-are-ignored-test
|
||||
(async done
|
||||
(let [page-uuid #uuid "99999999-9999-4999-8999-999999999998"
|
||||
block-uuid #uuid "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaab"
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:properties {:user.property/rating {:logseq.property/type :number}}
|
||||
:pages-and-blocks [{:page {:block/title "Property Ignore"}
|
||||
:blocks [{:block/title "task"
|
||||
:block/uuid block-uuid
|
||||
:build/properties {:user.property/rating 5}}]}]})
|
||||
page (db-test/find-page-by-title @conn "Property Ignore")
|
||||
content (str (page-marker (:block/uuid page)) "\n"
|
||||
(block-marker block-uuid) "\n"
|
||||
"- task\n"
|
||||
" rating:: 10")]
|
||||
(-> (markdown-mirror/<import-file-content! test-repo conn "pages/Property Ignore.md" content {})
|
||||
(p/then (fn [result]
|
||||
(is (= :skipped (:status result)))
|
||||
(is (= 5 (:logseq.property/value
|
||||
(:user.property/rating (d/entity @conn [:block/uuid block-uuid])))))))
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest two-way-inserted-block-gets-marker-through-suppressed-render-test
|
||||
(async done
|
||||
(let [{:keys [platform files]} (fake-platform)
|
||||
page-uuid #uuid "99999999-9999-4999-8999-999999999997"
|
||||
block-uuid #uuid "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaac"
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks [{:page {:block/title "Insert Page"
|
||||
:block/uuid page-uuid}
|
||||
:blocks [{:block/title "existing"
|
||||
:block/uuid block-uuid}]}]})
|
||||
content (str (page-marker page-uuid) "\n"
|
||||
(block-marker block-uuid) "\n"
|
||||
"- existing\n"
|
||||
"- inserted")]
|
||||
(-> (markdown-mirror/<import-file-content! test-repo conn "pages/Insert Page.md" content {:platform platform})
|
||||
(p/then (fn [result]
|
||||
(is (= :imported (:status result)))
|
||||
(is (some? (block-by-title @conn "inserted")))
|
||||
(is (string/includes? (get @files (page-path "pages/Insert Page.md"))
|
||||
"<!-- logseq:block "))))
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest two-way-inserted-block-page-ref-creates-page-test
|
||||
(async done
|
||||
(let [{:keys [platform]} (fake-platform)
|
||||
page-uuid #uuid "99999999-9999-4999-8999-999999999989"
|
||||
block-uuid #uuid "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa3"
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks [{:page {:block/title "Inserted Ref"
|
||||
:block/uuid page-uuid}
|
||||
:blocks [{:block/title "existing"
|
||||
:block/uuid block-uuid}]}]})
|
||||
content (str (page-marker page-uuid) "\n"
|
||||
(block-marker block-uuid) "\n"
|
||||
"- existing\n"
|
||||
"- [[foo]] test")]
|
||||
(-> (markdown-mirror/<import-file-content! test-repo conn "pages/Inserted Ref.md" content {:platform platform})
|
||||
(p/then (fn [result]
|
||||
(let [foo (db-test/find-page-by-title @conn "foo")
|
||||
block (first (filter #(block-title-includes? @conn (:block/uuid %) "[[foo]]")
|
||||
(map #(d/entity @conn (:e %))
|
||||
(d/datoms @conn :avet :block/page (:db/id (db-test/find-page-by-title @conn "Inserted Ref"))))))]
|
||||
(is (= :imported (:status result)))
|
||||
(is (some? foo))
|
||||
(is (some? block))
|
||||
(is (= #{(:db/id foo)} (set (map :db/id (:block/refs block))))))))
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest two-way-missing-block-marker-deletes-block-test
|
||||
(async done
|
||||
(let [page-uuid #uuid "99999999-9999-4999-8999-999999999996"
|
||||
keep-uuid #uuid "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaad"
|
||||
delete-uuid #uuid "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaae"
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks [{:page {:block/title "Delete Block"
|
||||
:block/uuid page-uuid}
|
||||
:blocks [{:block/title "keep"
|
||||
:block/uuid keep-uuid}
|
||||
{:block/title "delete"
|
||||
:block/uuid delete-uuid}]}]})
|
||||
content (str (page-marker page-uuid) "\n"
|
||||
(block-marker keep-uuid) "\n"
|
||||
"- keep")]
|
||||
(-> (markdown-mirror/<import-file-content! test-repo conn "pages/Delete Block.md" content {})
|
||||
(p/then (fn [result]
|
||||
(is (= :imported (:status result)))
|
||||
(is (some? (d/entity @conn [:block/uuid keep-uuid])))
|
||||
(is (nil? (d/entity @conn [:block/uuid delete-uuid])))))
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest two-way-block-move-is-ignored-test
|
||||
(async done
|
||||
(let [page-uuid #uuid "99999999-9999-4999-8999-999999999995"
|
||||
parent-uuid #uuid "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaf"
|
||||
child-uuid #uuid "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaab0"
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks [{:page {:block/title "Move Ignore"
|
||||
:block/uuid page-uuid}
|
||||
:blocks [{:block/title "parent"
|
||||
:block/uuid parent-uuid
|
||||
:build/children [{:block/title "child"
|
||||
:block/uuid child-uuid}]}]}]})
|
||||
child-before (d/entity @conn [:block/uuid child-uuid])
|
||||
content (str (page-marker page-uuid) "\n"
|
||||
(block-marker parent-uuid) "\n"
|
||||
"- parent\n"
|
||||
(block-marker child-uuid) "\n"
|
||||
"- child")]
|
||||
(-> (markdown-mirror/<import-file-content! test-repo conn "pages/Move Ignore.md" content {})
|
||||
(p/then (fn [result]
|
||||
(is (= :skipped (:status result)))
|
||||
(is (= (:db/id (:block/parent child-before))
|
||||
(:db/id (:block/parent (d/entity @conn [:block/uuid child-uuid])))))))
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest two-way-new-page-file-creates-page-test
|
||||
(async done
|
||||
(let [{:keys [platform files]} (fake-platform)
|
||||
conn (db-test/create-conn-with-blocks {:pages-and-blocks []})]
|
||||
(-> (markdown-mirror/<import-file-content! test-repo conn "pages/New Imported.md" "- hello" {:platform platform})
|
||||
(p/then (fn [result]
|
||||
(is (= :imported (:status result)))
|
||||
(is (some? (db-test/find-page-by-title @conn "New Imported")))
|
||||
(is (some? (block-by-title @conn "hello")))
|
||||
(is (string/includes? (get @files (page-path "pages/New Imported.md"))
|
||||
"<!-- logseq:page "))))
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest two-way-new-journal-file-creates-journal-test
|
||||
(async done
|
||||
(let [conn (db-test/create-conn-with-blocks {:pages-and-blocks []})]
|
||||
(-> (markdown-mirror/<import-file-content! test-repo conn "journals/2026_05_05.md" "- journal item" {})
|
||||
(p/then (fn [result]
|
||||
(is (= :imported (:status result)))
|
||||
(is (some? (db-test/find-journal-by-journal-day @conn 20260505)))
|
||||
(is (some? (block-by-title @conn "journal item")))))
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest two-way-new-file-existing-page-marker-fails-test
|
||||
(async done
|
||||
(let [existing-page-uuid #uuid "99999999-9999-4999-8999-999999999994"
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks [{:page {:block/title "Existing"
|
||||
:block/uuid existing-page-uuid}
|
||||
:blocks [{:block/title "existing"}]}]})
|
||||
content (str (page-marker existing-page-uuid) "\n- copied")]
|
||||
(-> (markdown-mirror/<import-file-content! test-repo conn "pages/New With Marker.md" content {})
|
||||
(p/then (fn [result]
|
||||
(is (= :error (:status result)))
|
||||
(is (= :new-file-has-page-marker (:reason result)))
|
||||
(is (nil? (db-test/find-page-by-title @conn "New With Marker")))))
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest two-way-file-delete-is-ignored-test
|
||||
(async done
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks [{:page {:block/title "Delete Event"}
|
||||
:blocks [{:block/title "safe"}]}]})]
|
||||
(-> (markdown-mirror/<handle-file-event! test-repo conn {:type :deleted
|
||||
:relative-path "pages/Delete Event.md"} {})
|
||||
(p/then (fn [result]
|
||||
(is (= :skipped (:status result)))
|
||||
(is (= :ignored-delete-event (:reason result)))
|
||||
(is (some? (db-test/find-page-by-title @conn "Delete Event")))
|
||||
(is (some? (block-by-title @conn "safe")))))
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest chokidar-watcher-imports-new-markdown-files-test
|
||||
(async done
|
||||
(let [{:keys [platform files]} (fake-platform)
|
||||
conn (db-test/create-conn-with-blocks {:pages-and-blocks []})
|
||||
handlers (atom {})
|
||||
watched (atom nil)
|
||||
closed? (atom false)
|
||||
watcher #js {}
|
||||
_ (set! (.-on watcher)
|
||||
(fn [event handler]
|
||||
(swap! handlers assoc event handler)
|
||||
watcher))
|
||||
_ (set! (.-close watcher)
|
||||
(fn []
|
||||
(reset! closed? true)))
|
||||
chokidar-watch! (fn [path opts]
|
||||
(reset! watched [path opts])
|
||||
watcher)
|
||||
relative-path "pages/Watcher New.md"
|
||||
storage-path (page-path relative-path)]
|
||||
(swap! files assoc storage-path "- from watcher")
|
||||
(-> (p/let [start-result (markdown-mirror/<start-file-watcher! test-repo conn {:platform platform
|
||||
:chokidar-watch! chokidar-watch!})
|
||||
event-result ((get @handlers "add") (str "/tmp/logseq/" storage-path))
|
||||
_ (markdown-mirror/stop-file-watcher! test-repo)]
|
||||
(is (= :watching (:status start-result)))
|
||||
(is (= [(str "/tmp/logseq/" (markdown-mirror/repo-mirror-dir test-repo))
|
||||
{:ignore-initial true
|
||||
:await-write-finish {:stability-threshold 200
|
||||
:poll-interval 50}}]
|
||||
@watched))
|
||||
(is (= :imported (:status event-result)))
|
||||
(is (some? (db-test/find-page-by-title @conn "Watcher New")))
|
||||
(is (some? (block-by-title @conn "from watcher")))
|
||||
(is (true? @closed?)))
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
Reference in New Issue
Block a user