فهرست منبع

Fix issues related to importing Zotero (#12313)

* Fix import Zotero not recognized as asset

* Fix the inconsistency between imported Zotero label and actual filename

* Import support for zotero linked file like using zotfile

* Fix zotero highlighting block error after importing from file

* compatible with old configuration

* fix zotero picture highlight block does not consider as asset after importing

* Reconstructed to meet bb requirements

* empty datascript to avoid Conflicting upsert error after importing file to db graph

* use external-file-name instead of adding new property

* fix zotero link file can't open in asset page

* compatible with commit 33db791

* compatible with windows path & support zotero path can be relative for ci testing

* add zotero importing test

* remove prn log

* remove useless line

* Revert commit 45ebb9e

Future imports will be performed in the worker, will no longer encounter these issues

* refactor: update today page check to use async block retrieval

* Revert "refactor: update today page check to use async block retrieval"

This reverts commit 6750333df16c3ddb52121f952c2e548c8d1dcf89.
megayu 3 هفته پیش
والد
کامیت
40c9c86129
17فایلهای تغییر یافته به همراه438 افزوده شده و 89 حذف شده
  1. 1 0
      deps/db/src/logseq/db/frontend/property.cljs
  2. 160 82
      deps/graph-parser/src/logseq/graph_parser/exporter.cljs
  3. 117 5
      deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs
  4. BIN
      deps/graph-parser/test/resources/book/it/Understanding EXPLAIN.pdf
  5. 53 0
      deps/graph-parser/test/resources/exporter-test-graph/assets/Understanding EXPLAIN.edn
  6. BIN
      deps/graph-parser/test/resources/exporter-test-graph/assets/Understanding EXPLAIN/8_69787518-9eb8-4fd0-ad3b-b394e28bb547_1769501975346.png
  7. 36 0
      deps/graph-parser/test/resources/exporter-test-graph/assets/zlib.edn
  8. BIN
      deps/graph-parser/test/resources/exporter-test-graph/assets/zlib/1_697873d7-5953-4753-897e-62ac47348634_1769501651181.png
  9. 2 0
      deps/graph-parser/test/resources/exporter-test-graph/journals/2026_01_01.md
  10. 3 0
      deps/graph-parser/test/resources/exporter-test-graph/logseq/config.edn
  11. 5 0
      deps/graph-parser/test/resources/exporter-test-graph/pages/Understanding EXPLAIN.md
  12. 20 0
      deps/graph-parser/test/resources/exporter-test-graph/pages/hls__Understanding EXPLAIN.md
  13. 15 0
      deps/graph-parser/test/resources/exporter-test-graph/pages/hls__zlib.md
  14. 5 0
      deps/graph-parser/test/resources/exporter-test-graph/pages/zlib.md
  15. BIN
      deps/graph-parser/test/resources/zotero/storage/RX5JS7SY/zlib.pdf
  16. 1 1
      src/main/frontend/components/block.cljs
  17. 20 1
      src/main/frontend/extensions/pdf/assets.cljs

+ 1 - 0
deps/db/src/logseq/db/frontend/property.cljs

@@ -532,6 +532,7 @@
                                                    :hide? false
                                                    :public? true}
                                           :queryable? true}
+     ;; need to rename for better alignment with practical purposes
      :logseq.property.asset/external-file-name {:title "External file name"
                                                 :schema {:type :string
                                                          :hide? true

+ 160 - 82
deps/graph-parser/src/logseq/graph_parser/exporter.cljs

@@ -936,7 +936,7 @@
       (when (and link id label)
         (when-let [zotero-data-dir (get-in config [:zotero/settings-v2 "default" :zotero-data-directory])]
           {:link (str "zotero://" link)
-           :path (node-path/join zotero-data-dir "storage" id label)
+           :path (path/path-join zotero-data-dir "storage" id label)
            :base label})))))
 
 (defn- walk-ast-blocks
@@ -945,13 +945,15 @@
   [config ast-blocks]
   (let [results (atom {:simple-queries []
                        :asset-links []
-                       :embeds []})]
+                       :embeds []
+                       :zotero-imported-files {}
+                       :zotero-linked-files []})]
     (walk/prewalk
      (fn [x]
        (cond
-         (and (vector? x)
-              (= "Link" (first x))
-              (let [path-or-map (second (:url (second x)))]
+        (and (vector? x)
+             (= "Link" (first x))
+             (let [path-or-map (second (:url (second x)))]
                 (cond
                   (string? path-or-map)
                   (or (common-config/local-relative-asset? path-or-map)
@@ -965,6 +967,19 @@
               (= "Macro" (first x))
               (= "embed" (:name (second x))))
          (swap! results update :embeds conj x)
+        (and (vector? x)
+             (= "Macro" (first x))
+             (= "zotero-imported-file" (:name (second x))))
+        (let [[item-key filename] (:arguments (second x))]
+          (when (and item-key filename)
+            (swap! results update :zotero-imported-files assoc item-key (common-util/safe-read-string filename))))
+        (and (vector? x)
+             (= "Macro" (first x))
+             (= "zotero-linked-file" (:name (second x))))
+        (let [[relative-path] (:arguments (second x))
+              parsed-path (common-util/safe-read-string relative-path)]
+          (when (string? parsed-path)
+            (swap! results update :zotero-linked-files conj parsed-path)))
          (and (vector? x)
               (= "Macro" (first x))
               (= "query" (:name (second x))))
@@ -1231,10 +1246,10 @@
 (defn- build-pdf-annotations-tx
   "Builds tx for pdf annotations when a pdf has an annotations EDN file under assets/"
   [parent-asset-path assets parent-asset pdf-annotation-pages opts]
-  (let [asset-edn-path (path/path-normalize
-                        (node-path/join common-config/local-assets-dir
-                                        (safe-sanitize-file-name
-                                         (node-path/basename (string/replace-first parent-asset-path #"(?i)\.pdf$" ".edn")))))
+  (let [asset-edn-path (path/path-join
+                        common-config/local-assets-dir
+                        (safe-sanitize-file-name
+                         (node-path/basename (string/replace-first parent-asset-path #"(?i)\.pdf$" ".edn"))))
         asset-md-name (str "hls__" (safe-sanitize-file-name
                                     (node-path/basename (string/replace-first parent-asset-path #"(?i)\.pdf$" ".md"))))]
     (when-let [asset-edn-map (get @assets asset-edn-path)]
@@ -1242,79 +1257,127 @@
         (concat txs
                 (build-pdf-annotations-tx* asset-edn-map (get @pdf-annotation-pages asset-md-name) parent-asset image-asset-name-to-uuids opts))))))
 
+(defn- resolve-asset-data
+  [asset-link user-config linked-files linked-base-dir zotero-imported-files]
+  (let [link-map (second asset-link)
+        path* (-> link-map :url second)
+        zotero-path-data (when (map? path*)
+                           (get-zotero-local-pdf-path user-config link-map))
+        zotero-asset? (some? zotero-path-data)
+        linked-relative (when (and linked-files zotero-asset? (seq @linked-files))
+                          (let [value (first @linked-files)]
+                            (swap! linked-files rest)
+                            (string/replace-first value "attachments:" "")))
+        linked-base (when (string? linked-relative)
+                      (node-path/basename linked-relative))
+        linked-path (when (and (string? linked-relative)
+                               (string? linked-base-dir)
+                               (not (string/blank? linked-base-dir)))
+                      (node-path/join linked-base-dir linked-relative))
+        {:keys [path link base]} (cond
+                                   linked-path {:path linked-path
+                                                :link (:link zotero-path-data)
+                                                :base linked-base}
+                                   zotero-asset? zotero-path-data
+                                   :else {:path path*})
+        asset-name (cond
+                     linked-path base
+                     zotero-asset? (or (get zotero-imported-files (last (string/split link #"/"))) base)
+                     :else (some-> path asset-path->name))
+        path (cond
+               linked-path path
+               (and zotero-asset? asset-name) (string/replace path #"[^/]+$" asset-name)
+               :else path)
+        asset-link-or-name (or link asset-name)
+        asset-path (when zotero-asset?
+                     (if linked-path
+                       (str "zotero-link://" linked-relative)
+                       (str "zotero-path://" (string/join ns-util/namespace-char [(last (string/split link #"/")) asset-name]))))]
+    {:asset-link-or-name asset-link-or-name
+     :asset-name asset-name
+     :asset-path asset-path
+     :path path
+     :zotero-asset? zotero-asset?}))
+
+(defn- ensure-asset-data!
+  [assets asset-link-or-name path asset-path <get-file-stat]
+  (when (and asset-link-or-name
+             (not (get @assets asset-link-or-name))
+             (string/ends-with? path ".pdf")
+             (fn? <get-file-stat))
+    (-> (p/let [^js stat (<get-file-stat path)]
+          (swap! assets assoc asset-link-or-name
+                 {:asset-id (d/squuid)
+                  :type "pdf"
+                  ;; avoid using the real checksum since it could be the same with in-graph asset
+                  :checksum "0000000000000000000000000000000000000000000000000000000000000000"
+                  :size (.-size stat)
+                  :external-url (or asset-link-or-name path)
+                  :external-file-name asset-path}))
+        (p/catch (fn [error]
+                   (js/console.error error))))))
+
+(defn- build-asset-tx
+  [asset-data asset-name asset-link-or-name asset-link pdf-annotation-pages opts assets zotero-asset?]
+  (let [new-asset (merge (build-new-asset asset-data)
+                         {:block/title (db-asset/asset-name->title (node-path/basename asset-name))
+                          :block/uuid (get-asset-block-id assets asset-link-or-name)}
+                         (when-let [metadata (not-empty (common-util/safe-read-map-string (:metadata (second asset-link))))]
+                           {:logseq.property.asset/resize-metadata metadata}))
+        pdf-annotations-path (if (and zotero-asset? (string? asset-name))
+                               (path/path-join common-config/local-assets-dir asset-name)
+                               (or asset-name asset-link-or-name))
+        pdf-annotations-tx (when (= "pdf" (path/file-ext pdf-annotations-path))
+                             (build-pdf-annotations-tx pdf-annotations-path assets new-asset pdf-annotation-pages opts))
+        asset-tx (concat [new-asset] pdf-annotations-tx)]
+    ;; (prn :asset-added! (node-path/basename asset-name))
+    ;; (cljs.pprint/pprint asset-link)
+    ;; (prn :debug :asset-tx asset-tx)
+    (swap! assets assoc-in [asset-link-or-name :asset-created?] true)
+    {:asset-name-uuid [asset-link-or-name (:block/uuid new-asset)]
+     :asset-tx asset-tx}))
+
 (defn- <handle-assets-in-block
   "If a block contains assets, creates them as #Asset nodes in the Asset page and references them in the block."
-  [block {:keys [asset-links]} {:keys [assets ignored-assets pdf-annotation-pages]} {:keys [notify-user <get-file-stat user-config] :as opts}]
-  (if (seq asset-links)
-    (p/let [asset-maps* (p/all (map
-                                (fn [asset-link]
-                                  (p/let [path* (-> asset-link second :url second)
-                                          zotero-asset? (when (map? path*)
-                                                          (= "zotero" (:protocol (second (:url (second asset-link))))))
-                                          {:keys [path link base]} (if (map? path*)
-                                                                     (get-zotero-local-pdf-path user-config (second asset-link))
-                                                                     {:path path*})
-                                          asset-name (some-> path asset-path->name)
-                                          asset-link-or-name (or link (some-> path asset-path->name))
-                                          asset-data* (when asset-link-or-name (get @assets asset-link-or-name))
-                                          _ (when (and asset-link-or-name
-                                                       (not asset-data*)
-                                                       (string/ends-with? path ".pdf")
-                                                       (fn? <get-file-stat)) ; external pdf
-                                              (->
-                                               (p/let [^js stat (<get-file-stat path)]
-                                                 (swap! assets assoc asset-link-or-name
-                                                        {:asset-id (d/squuid)
-                                                         :type "pdf"
-                                                         ;; avoid using the real checksum since it could be the same with in-graph asset
-                                                         :checksum "0000000000000000000000000000000000000000000000000000000000000000"
-                                                         :size (.-size stat)
-                                                         :external-url (or link path)
-                                                         :external-file-name base}))
-                                               (p/catch (fn [error]
-                                                          (js/console.error error)))))
-                                          asset-data (when asset-link-or-name (get @assets asset-link-or-name))]
-                                    (if asset-data
-                                      (cond
-                                        (not (get-asset-block-id assets asset-link-or-name))
-                                        (notify-user {:msg (str "Skipped creating asset " (pr-str asset-link-or-name) " because it has no asset id")
-                                                      :level :error})
-
-                                        ;; If asset tx is already built, no need to do it again
-                                        (:asset-created? asset-data)
-                                        {:asset-name-uuid [asset-link-or-name (:asset-id asset-data)]}
-
-                                        :else
-                                        (let [new-asset (merge (build-new-asset asset-data)
-                                                               {:block/title (db-asset/asset-name->title (node-path/basename asset-name))
-                                                                :block/uuid (get-asset-block-id assets asset-link-or-name)}
-                                                               (when-let [metadata (not-empty (common-util/safe-read-map-string (:metadata (second asset-link))))]
-                                                                 {:logseq.property.asset/resize-metadata metadata}))
-                                              pdf-annotations-tx (when (= "pdf" (path/file-ext asset-link-or-name))
-                                                                   (build-pdf-annotations-tx asset-link-or-name assets new-asset pdf-annotation-pages opts))
-                                              asset-tx (concat [new-asset] pdf-annotations-tx)]
-                                          ;; (prn :asset-added! (node-path/basename asset-name))
-                                          ;; (cljs.pprint/pprint asset-link)
-                                          ;; (prn :debug :asset-tx asset-tx)
-                                          (swap! assets assoc-in [asset-link-or-name :asset-created?] true)
-                                          {:asset-name-uuid [asset-link-or-name (:block/uuid new-asset)]
-                                           :asset-tx asset-tx}))
-                                      (when-not zotero-asset? ; no need to report warning for zotero managed pdf files
-                                        (swap! ignored-assets conj
-                                               {:reason "No asset data found for this asset path"
-                                                :path (-> asset-link second :url second)
-                                                :location {:block (:block/title block)}})
-                                        nil))))
-                                asset-links))
-            asset-maps (remove nil? asset-maps*)
-            asset-blocks (mapcat :asset-tx asset-maps)
-            asset-names-to-uuids
-            (into {} (map :asset-name-uuid asset-maps))]
-      (cond-> {:block
-               (update block :block/title update-asset-links-in-block-title asset-names-to-uuids ignored-assets)}
-        (seq asset-blocks)
-        (assoc :asset-blocks-tx asset-blocks)))
-    (p/resolved {:block block})))
+  [block {:keys [asset-links zotero-imported-files zotero-linked-files]} {:keys [assets ignored-assets pdf-annotation-pages]} {:keys [notify-user <get-file-stat user-config] :as opts}]
+  (let [linked-files (when (seq zotero-linked-files) (atom zotero-linked-files))
+        linked-base-dir (when linked-files
+                          (get-in user-config [:zotero/settings-v2 "default" :zotero-linked-attachment-base-directory]))]
+    (if (seq asset-links)
+      (p/let [asset-maps* (p/all (map
+                                  (fn [asset-link]
+                                    (p/let [{:keys [asset-link-or-name asset-name asset-path path zotero-asset?]}
+                                            (resolve-asset-data asset-link user-config linked-files linked-base-dir zotero-imported-files)
+                                            _ (ensure-asset-data! assets asset-link-or-name path asset-path <get-file-stat)
+                                            asset-data (when asset-link-or-name (get @assets asset-link-or-name))]
+                                      (if asset-data
+                                        (cond
+                                          (not (get-asset-block-id assets asset-link-or-name))
+                                          (notify-user {:msg (str "Skipped creating asset " (pr-str asset-link-or-name) " because it has no asset id")
+                                                        :level :error})
+
+                                          ;; If asset tx is already built, no need to do it again
+                                          (:asset-created? asset-data)
+                                          {:asset-name-uuid [asset-link-or-name (:asset-id asset-data)]}
+
+                                          :else
+                                          (build-asset-tx asset-data asset-name asset-link-or-name asset-link pdf-annotation-pages opts assets zotero-asset?))
+                                        (when-not zotero-asset? ; no need to report warning for zotero managed pdf files
+                                          (swap! ignored-assets conj
+                                                 {:reason "No asset data found for this asset path"
+                                                  :path (-> asset-link second :url second)
+                                                  :location {:block (:block/title block)}})
+                                          nil))))
+                                  asset-links))
+              asset-maps (remove nil? asset-maps*)
+              asset-blocks (mapcat :asset-tx asset-maps)
+              asset-names-to-uuids
+              (into {} (map :asset-name-uuid asset-maps))]
+        (cond-> {:block
+                 (update block :block/title update-asset-links-in-block-title asset-names-to-uuids ignored-assets)}
+          (seq asset-blocks)
+          (assoc :asset-blocks-tx asset-blocks)))
+      (p/resolved {:block block}))))
 
 (defn- handle-quotes
   "If a block contains a quote, convert block to #Quote node"
@@ -1361,7 +1424,7 @@
           {block-after-built-in-props :block deadline-properties-tx :properties-tx}
           (update-block-deadline-and-scheduled block page-names-to-uuids options)
           {block-after-assets :block :keys [asset-blocks-tx]}
-          (<handle-assets-in-block block-after-built-in-props walked-ast-blocks import-state (select-keys options [:log-fn :notify-user :<get-file-stat]))
+          (<handle-assets-in-block block-after-built-in-props walked-ast-blocks import-state (select-keys options [:log-fn :notify-user :<get-file-stat :user-config]))
           ;; :block/page should be [:block/page NAME]
 
           journal-page-created-at (some-> (:block/page block*) second journal-created-ats)
@@ -1978,6 +2041,21 @@
                                  :level :error
                                  :ex-data {:error error}}))))))
 
+(defn- resolve-zotero-config-path
+  [config config-file]
+  (let [config-path (:path config-file)
+        base-dir (when (and (string? config-path)
+                            (node-path/isAbsolute config-path))
+                   ;; config.edn lives in <graph-root>/logseq/config.edn
+                   (node-path/dirname (node-path/dirname config-path)))
+        to-abs (fn [p]
+                 (if (and base-dir (string? p) (not (string/blank? p)) (not (node-path/isAbsolute p)))
+                   (path/path-join base-dir p)
+                   p))]
+    (-> config
+        (update-in [:zotero/settings-v2 "default" :zotero-data-directory] to-abs)
+        (update-in [:zotero/settings-v2 "default" :zotero-linked-attachment-base-directory] to-abs))))
+
 (defn export-config-file
   "Exports logseq/config.edn by saving to database and setting any properties related to config"
   [repo-or-conn config-file <read-file {:keys [<save-file notify-user default-config]
@@ -1990,7 +2068,7 @@
                             ;; Converts a file graph config.edn for use with DB graphs. Unlike common-config/create-config-for-db-graph,
                             ;; manually dissoc deprecated keys for config to be valid
                             (pretty-print-dissoc % (keys common-config/file-only-config)))
-                (let [config (edn/read-string %)]
+                (let [config (resolve-zotero-config-path (edn/read-string %) config-file)]
                   (when-let [title-format (or (:journal/page-title-format config) (:date-formatter config))]
                     (ldb/transact! repo-or-conn [{:db/ident :logseq.class/Journal
                                                   :logseq.property.journal/title-format title-format}]))

+ 117 - 5
deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs

@@ -125,6 +125,11 @@
         options' (merge default-export-options
                         {:user-options (merge {:convert-all-tags? false} (dissoc options :assets :verbose))
                         ;; asset file options
+                         :<get-file-stat (fn [path]
+                                           (let [abs-path (if (node-path/isAbsolute path)
+                                                            path
+                                                            (node-path/resolve file-graph-dir path))]
+                                             (.stat (js/require "fs/promises") abs-path)))
                          :<read-and-copy-asset (fn [file *assets buffer-handler]
                                                  (<read-and-copy-asset file *assets buffer-handler assets))}
                         (select-keys options [:verbose]))]
@@ -210,17 +215,17 @@
 
       ;; Counts
       ;; Includes journals as property values e.g. :logseq.property/deadline
-      (is (= 32 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Journal]] @conn))))
+      (is (= 33 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Journal]] @conn))))
 
-      (is (= 5 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Asset]] @conn))))
+      (is (= 9 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Asset]] @conn))))
       (is (= 5 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Task]] @conn))))
       (is (= 4 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Query]] @conn))))
       (is (= 2 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Card]] @conn))))
       (is (= 5 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Quote-block]] @conn))))
-      (is (= 2 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Pdf-annotation]] @conn))))
+      (is (= 7 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Pdf-annotation]] @conn))))
 
       ;; Properties and tags aren't included in this count as they aren't a Page
-      (is (= 11
+      (is (= 13
              (->> (d/q '[:find [?b ...]
                          :where
                          [?b :block/title]
@@ -239,7 +244,8 @@
       (is (= 0 (count @(:ignored-properties import-state))) "No ignored properties")
       (is (= 0 (count @(:ignored-assets import-state))) "No ignored assets")
       (is (= 1 (count @(:ignored-files import-state))) "Ignore .edn for now")
-      (is (= 5 (count @assets))))
+      ;; 2 zotero pdf are external files so not counted here
+      (is (= 7 (count @assets))))
 
     (testing "logseq files"
       (is (= ".foo {}\n"
@@ -419,6 +425,28 @@
               :logseq.property.asset/resize-metadata {:height 288, :width 252}}
              (db-test/readable-properties (db-test/find-block-by-content @conn "greg-popovich-thumbs-up_1704749687791_0")))
           "Asset has correct properties")
+      (is (= {:block/tags [:logseq.class/Asset]
+              :logseq.property.asset/type "pdf"
+              :logseq.property.asset/external-url "zotero://select/library/items/QDM8H6EH"
+              :logseq.property.asset/external-file-name "zotero-link://it/Understanding EXPLAIN.pdf"}
+             (select-keys
+              (db-test/readable-properties (db-test/find-block-by-content @conn "Understanding EXPLAIN"))
+              [:block/tags
+               :logseq.property.asset/type
+               :logseq.property.asset/external-url
+               :logseq.property.asset/external-file-name]))
+          "Zotero linked pdf asset has correct external path info")
+      (is (= {:block/tags [:logseq.class/Asset]
+              :logseq.property.asset/type "pdf"
+              :logseq.property.asset/external-url "zotero://select/library/items/RX5JS7SY"
+              :logseq.property.asset/external-file-name "zotero-path://RX5JS7SY/zlib.pdf"}
+             (select-keys
+              (db-test/readable-properties (db-test/find-block-by-content @conn "zlib"))
+              [:block/tags
+               :logseq.property.asset/type
+               :logseq.property.asset/external-url
+               :logseq.property.asset/external-file-name]))
+          "Zotero imported pdf asset has correct external path info")
       (is (= (d/entity @conn :logseq.class/Asset)
              (:block/page (db-test/find-block-by-content @conn "greg-popovich-thumbs-up_1704749687791_0")))
           "Imported into Asset page")
@@ -447,6 +475,90 @@
                           db-test/readable-properties)
                      :logseq.property.pdf/hl-value :logseq.property/ls-type))
           "Pdf area highlight has correct properties")
+      (is (= {:block/tags [:logseq.class/Pdf-annotation]
+              :logseq.property/asset "Understanding EXPLAIN"
+              :logseq.property.pdf/hl-color :logseq.property/color.yellow
+              :logseq.property.pdf/hl-page 6}
+             (select-keys
+              (db-test/readable-properties (db-test/find-block-by-content @conn #"EXPLAIN is a really nice command"))
+              [:block/tags
+               :logseq.property/asset
+               :logseq.property.pdf/hl-color
+               :logseq.property.pdf/hl-page]))
+          "Zotero linked pdf text highlight links to correct asset")
+      (is (= {:block/tags [:logseq.class/Pdf-annotation]
+              :logseq.property/asset "zlib"
+              :logseq.property.pdf/hl-color :logseq.property/color.red
+              :logseq.property.pdf/hl-page 1}
+             (select-keys
+              (db-test/readable-properties (db-test/find-block-by-content @conn #"The zlib library is a general purpose data compression library"))
+              [:block/tags
+               :logseq.property/asset
+               :logseq.property.pdf/hl-color
+               :logseq.property.pdf/hl-page]))
+          "Zotero imported pdf text highlight links to correct asset")
+      (let [area-hl (d/q '[:find (pull ?b [:block/title
+                                           {:block/tags [:db/ident]}
+                                           {:logseq.property/asset [:block/title]}
+                                           {:logseq.property.pdf/hl-image [:block/title]}
+                                           {:logseq.property.pdf/hl-color [:db/ident]}
+                                           :logseq.property.pdf/hl-type
+                                           :logseq.property.pdf/hl-page]) .
+                           :where
+                           [?asset :block/title "Understanding EXPLAIN"]
+                           [?asset :block/tags :logseq.class/Asset]
+                           [?b :block/title "[:span]"]
+                           [?b :logseq.property/asset ?asset]]
+                         @conn)]
+        (is (= {:logseq.property.pdf/hl-color :logseq.property/color.green
+                :logseq.property.pdf/hl-page 8
+                :block/tags [:logseq.class/Pdf-annotation]
+                :logseq.property/asset "Understanding EXPLAIN"
+                :logseq.property.pdf/hl-image "pdf area highlight"
+                :logseq.property.pdf/hl-type :area}
+               (-> area-hl
+                   (update :block/tags #(mapv :db/ident %))
+                   (update :logseq.property/asset #(:block/title %))
+                   (update :logseq.property.pdf/hl-color #(:db/ident %))
+                   (update :logseq.property.pdf/hl-image #(:block/title %))
+                   (select-keys [:block/tags
+                                 :logseq.property/asset
+                                 :logseq.property.pdf/hl-color
+                                 :logseq.property.pdf/hl-page
+                                 :logseq.property.pdf/hl-image
+                                 :logseq.property.pdf/hl-type])))
+            "Zotero linked pdf area highlight links to correct asset"))
+      (let [area-hl (d/q '[:find (pull ?b [:block/title
+                                           {:block/tags [:db/ident]}
+                                           {:logseq.property/asset [:block/title]}
+                                           {:logseq.property.pdf/hl-image [:block/title]}
+                                           {:logseq.property.pdf/hl-color [:db/ident]}
+                                           :logseq.property.pdf/hl-type
+                                           :logseq.property.pdf/hl-page]) .
+                           :where
+                           [?asset :block/title "zlib"]
+                           [?asset :block/tags :logseq.class/Asset]
+                           [?b :block/title "[:span]"]
+                           [?b :logseq.property/asset ?asset]]
+                         @conn)]
+        (is (= {:logseq.property.pdf/hl-color :logseq.property/color.blue
+                :logseq.property.pdf/hl-page 1
+                :block/tags [:logseq.class/Pdf-annotation]
+                :logseq.property/asset "zlib"
+                :logseq.property.pdf/hl-image "pdf area highlight"
+                :logseq.property.pdf/hl-type :area}
+               (-> area-hl
+                   (update :block/tags #(mapv :db/ident %))
+                   (update :logseq.property/asset #(:block/title %))
+                   (update :logseq.property.pdf/hl-color #(:db/ident %))
+                   (update :logseq.property.pdf/hl-image #(:block/title %))
+                   (select-keys [:block/tags
+                                 :logseq.property/asset
+                                 :logseq.property.pdf/hl-color
+                                 :logseq.property.pdf/hl-page
+                                 :logseq.property.pdf/hl-image
+                                 :logseq.property.pdf/hl-type])))
+            "Zotero imported pdf area highlight links to correct asset"))
 
       ;; Quotes
       (is (= {:block/tags [:logseq.class/Quote-block]

BIN
deps/graph-parser/test/resources/book/it/Understanding EXPLAIN.pdf


+ 53 - 0
deps/graph-parser/test/resources/exporter-test-graph/assets/Understanding EXPLAIN.edn

@@ -0,0 +1,53 @@
+{:highlights [{:id #uuid "697874ba-26e9-4e8a-a55d-0639d337dcc6",
+               :page 6,
+               :position {:bounding {:x1 67.03125,
+                                     :y1 344.90625,
+                                     :x2 612.7423095703125,
+                                     :y2 379.3333740234375,
+                                     :width 702,
+                                     :height 993.418487394958},
+                          :rects ({:x1 67.03125,
+                                   :y1 344.90625,
+                                   :x2 612.7423095703125,
+                                   :y2 364.2396240234375,
+                                   :width 702,
+                                   :height 993.418487394958}
+                                  {:x1 67.03125,
+                                   :y1 360,
+                                   :x2 238.0740203857422,
+                                   :y2 379.3333740234375,
+                                   :width 702,
+                                   :height 993.418487394958}),
+                          :page 6},
+               :content {:text "EXPLAIN is a really nice command that gives you lots of information but it's often easy to not know what to do with all this"},
+               :properties {:color "yellow"}}
+              {:id #uuid "69787518-9eb8-4fd0-ad3b-b394e28bb547",
+               :page 8,
+               :position {:bounding {:x1 84,
+                                     :y1 163,
+                                     :x2 637,
+                                     :y2 327,
+                                     :width 702,
+                                     :height 993.418487394958},
+                          :rects (),
+                          :page 8},
+               :content {:text "[:span]", :image 1769501975346},
+               :properties {:color "green"}}
+              {:id #uuid "69787540-7a52-40af-a70a-cedd315934cf",
+               :page 42,
+               :position {:bounding {:x1 119.05208587646484,
+                                     :y1 95.67709350585938,
+                                     :x2 243.25509643554688,
+                                     :y2 133.01040649414062,
+                                     :width 702,
+                                     :height 993.418487394958},
+                          :rects ({:x1 119.05208587646484,
+                                   :y1 95.67709350585938,
+                                   :x2 243.25509643554688,
+                                   :y2 133.01040649414062,
+                                   :width 702,
+                                   :height 993.418487394958}),
+                          :page 42},
+               :content {:text "Conclusion"},
+               :properties {:color "blue"}}],
+ :extra {:page 8}}

BIN
deps/graph-parser/test/resources/exporter-test-graph/assets/Understanding EXPLAIN/8_69787518-9eb8-4fd0-ad3b-b394e28bb547_1769501975346.png


+ 36 - 0
deps/graph-parser/test/resources/exporter-test-graph/assets/zlib.edn

@@ -0,0 +1,36 @@
+{:highlights [{:id #uuid "69787399-f51c-45a2-8971-78dc6c66a0ec",
+               :page 1,
+               :position {:bounding {:x1 123.89583587646484,
+                                     :y1 164.3125,
+                                     :x2 438.8349304199219,
+                                     :y2 180.9791717529297,
+                                     :width 702,
+                                     :height 908.470588235294},
+                          :rects ({:x1 123.89583587646484,
+                                   :y1 164.3125,
+                                   :x2 438.8349304199219,
+                                   :y2 180.9791717529297,
+                                   :width 702,
+                                   :height 908.470588235294}
+                                  {:x1 143.625,
+                                   :y1 166.9791717529297,
+                                   :x2 432.40625,
+                                   :y2 178.4479217529297,
+                                   :width 702,
+                                   :height 908.470588235294}),
+                          :page 1},
+               :content {:text "The zlib library is a general purpose data compression library."},
+               :properties {:color "red"}}
+              {:id #uuid "697873d7-5953-4753-897e-62ac47348634",
+               :page 1,
+               :position {:bounding {:x1 116,
+                                     :y1 530,
+                                     :x2 627,
+                                     :y2 690,
+                                     :width 702,
+                                     :height 908.470588235294},
+                          :rects (),
+                          :page 1},
+               :content {:text "[:span]", :image 1769501651181},
+               :properties {:color "blue"}}],
+ :extra {:page 2}}

BIN
deps/graph-parser/test/resources/exporter-test-graph/assets/zlib/1_697873d7-5953-4753-897e-62ac47348634_1769501651181.png


+ 2 - 0
deps/graph-parser/test/resources/exporter-test-graph/journals/2026_01_01.md

@@ -0,0 +1,2 @@
+- [[zlib]]
+- [[Understanding EXPLAIN]]

+ 3 - 0
deps/graph-parser/test/resources/exporter-test-graph/logseq/config.edn

@@ -399,4 +399,7 @@
  ;;   - :triple-lowbar (default)
  ;;      ;use triple underscore `___` for slash `/` in page title
  ;;      ;use Percent-encoding for other invalid characters
+
+ ;; use relative path here only for ci test, won't work on the Electron client
+ :zotero/settings-v2 {"default" {:type-id "1234567", :prefer-citekey? false, :attachments-block-text "Attachments", :page-insert-prefix "", :extra-tags "", :zotero-linked-attachment-base-directory "../book", :zotero-data-directory "../zotero"}}
  :file/name-format :triple-lowbar}

+ 5 - 0
deps/graph-parser/test/resources/exporter-test-graph/pages/Understanding EXPLAIN.md

@@ -0,0 +1,5 @@
+- Attachments
+	- [Understanding EXPLAIN.pdf](zotero://select/library/items/QDM8H6EH) {{zotero-linked-file "it/Understanding EXPLAIN.pdf"}}
+- Notes
+	- ((697874ba-26e9-4e8a-a55d-0639d337dcc6)) definition
+	- ((69787518-9eb8-4fd0-ad3b-b394e28bb547)) select

+ 20 - 0
deps/graph-parser/test/resources/exporter-test-graph/pages/hls__Understanding EXPLAIN.md

@@ -0,0 +1,20 @@
+file:: [Understanding EXPLAIN.pdf](../../book/it/Understanding EXPLAIN.pdf)
+file-path:: ../../book/it/Understanding EXPLAIN.pdf
+
+- EXPLAIN is a really nice command that gives you lots of information but it's often easy to not know what to do with all this
+  ls-type:: annotation
+  hl-page:: 6
+  hl-color:: yellow
+  id:: 697874ba-26e9-4e8a-a55d-0639d337dcc6
+- [:span]
+  ls-type:: annotation
+  hl-page:: 8
+  hl-color:: green
+  id:: 69787518-9eb8-4fd0-ad3b-b394e28bb547
+  hl-type:: area
+  hl-stamp:: 1769501975346
+- Conclusion
+  ls-type:: annotation
+  hl-page:: 42
+  hl-color:: blue
+  id:: 69787540-7a52-40af-a70a-cedd315934cf

+ 15 - 0
deps/graph-parser/test/resources/exporter-test-graph/pages/hls__zlib.md

@@ -0,0 +1,15 @@
+file:: [zlib.pdf](../../zotero/RX5JS7SY/zlib.pdf)
+file-path:: ../../zotero/storage/RX5JS7SY/zlib.pdf
+
+- The zlib library is a general purpose data compression library.
+  ls-type:: annotation
+  hl-page:: 1
+  hl-color:: red
+  id:: 69787399-f51c-45a2-8971-78dc6c66a0ec
+- [:span]
+  ls-type:: annotation
+  hl-page:: 1
+  hl-color:: blue
+  id:: 697873d7-5953-4753-897e-62ac47348634
+  hl-type:: area
+  hl-stamp:: 1769501651181

+ 5 - 0
deps/graph-parser/test/resources/exporter-test-graph/pages/zlib.md

@@ -0,0 +1,5 @@
+- Attachments
+	- [PDF](zotero://select/library/items/RX5JS7SY) {{zotero-imported-file RX5JS7SY, "zlib.pdf"}}
+- Notes
+	- ((69787399-f51c-45a2-8971-78dc6c66a0ec)) definition
+	- ((697873d7-5953-4753-897e-62ac47348634)) links

BIN
deps/graph-parser/test/resources/zotero/storage/RX5JS7SY/zlib.pdf


+ 1 - 1
src/main/frontend/components/block.cljs

@@ -370,7 +370,7 @@
   [e block href]
   (let [href (if-let [url (:logseq.property.asset/external-url block)]
                (if (string/starts-with? url "zotero://")
-                 (zotero/zotero-full-path (last (string/split url #"/")) (:logseq.property.asset/external-file-name block))
+                 (pdf-assets/get-zotero-local-pdf-path (:logseq.property.asset/external-file-name block) :id (last (string/split url #"/")))
                  url)
                href)]
     (when-let [s (or href (some-> (.-target e) (.-dataset) (.-href)))]

+ 20 - 1
src/main/frontend/extensions/pdf/assets.cljs

@@ -198,12 +198,31 @@
        (ref/->block-ref (:block/uuid ref-block))
        :owner-window (pdf-windows/resolve-own-window viewer)))))
 
+(defn get-zotero-local-pdf-path
+  [path & {:keys [id]}]
+  (let [zotero-config (get-in (state/sub-config) [:zotero/settings-v2 "default"])
+        zotero-data-directory (:zotero-data-directory zotero-config)
+        zotero-linked-attachment-base-directory (:zotero-linked-attachment-base-directory zotero-config)
+        relative-path (subs path 14)]
+    (cond
+      (string/starts-with? path "zotero-link://")
+      (str "file://" (util/node-path.join zotero-linked-attachment-base-directory relative-path))
+
+      (string/starts-with? path "zotero-path://")
+      (str "file://" (util/node-path.join zotero-data-directory "storage" relative-path))
+
+      :else ;; compatible with commit 33db791
+      (str "file://" (util/node-path.join zotero-data-directory "storage" id path)))))
+
 (defn db-based-open-block-ref!
   [block]
   (let [hl-value (:logseq.property.pdf/hl-value block)
         asset (:logseq.property/asset block)
         external-url (:logseq.property.asset/external-url asset)
-        file-path (or external-url (str "../assets/" (:block/uuid asset) ".pdf"))]
+        file-path (or external-url (str "../assets/" (:block/uuid asset) ".pdf"))
+        file-path (if (string/starts-with? file-path "zotero://")
+                    (get-zotero-local-pdf-path (:logseq.property.asset/external-file-name asset))
+                    file-path)]
     (if asset
       (->
        (p/let [href (assets-handler/<make-asset-url file-path)]