From e286370c3676e431f9f0d7822db98b2ed3b2be4f Mon Sep 17 00:00:00 2001 From: megayu Date: Thu, 14 May 2026 19:13:07 +0800 Subject: [PATCH 1/3] Fix Windows file URL opening for block links (#12646) * fix: preserve Windows file URL handling in path functions * fix: improve file path handling for Windows file URLs * fix: correct regex for Windows file URL handling * improve file path compatibility --- deps/common/src/logseq/common/path.cljs | 19 ++++++++++-- deps/common/test/logseq/common/path_test.cljs | 15 +++++++++- src/main/frontend/components/block.cljs | 29 ++++++++++++++----- 3 files changed, 53 insertions(+), 10 deletions(-) diff --git a/deps/common/src/logseq/common/path.cljs b/deps/common/src/logseq/common/path.cljs index 1a4b5031e1..c299da88f8 100644 --- a/deps/common/src/logseq/common/path.cljs +++ b/deps/common/src/logseq/common/path.cljs @@ -130,6 +130,12 @@ []) (join-fn)))) +(defn- preserve-file-url-win-drive + [scheme encoded-path] + (cond-> encoded-path + (= scheme "file:") + (string/replace #"^/([a-zA-Z])%3[Aa](?=/|$)" "/$1:"))) + (defn url-join "Segments are not URL-encoded" [base-url & segments] @@ -138,7 +144,8 @@ path (.-pathname url) domain (or (not-empty (.-host url)) (if (string/starts-with? path "/") "" "/")) - encoded-new-path (apply uri-path-join-internal path segments)] + encoded-new-path (->> (apply uri-path-join-internal path segments) + (preserve-file-url-win-drive scheme))] (str scheme "//" domain encoded-new-path))) (defn path-join @@ -180,7 +187,8 @@ scheme (.-protocol url) domain (or (not-empty (.-host url)) "/") path (.-pathname url) - encoded-new-path (uri-path-join-internal path)] + encoded-new-path (->> (uri-path-join-internal path) + (preserve-file-url-win-drive scheme))] (str scheme "//" domain encoded-new-path))) (defn path-normalize @@ -219,6 +227,13 @@ (str "//" host path))) original-url)) +(defn file-url-or-path->path + "Converts a file URL to a local filesystem path." + [s] + (if (is-file-url? s) + (url-to-path s) + s)) + (defn trim-dir-prefix "Trim dir prefix from path" [base-path sub-path] diff --git a/deps/common/test/logseq/common/path_test.cljs b/deps/common/test/logseq/common/path_test.cljs index 8563ab0b30..a1f82e651e 100644 --- a/deps/common/test/logseq/common/path_test.cljs +++ b/deps/common/test/logseq/common/path_test.cljs @@ -21,15 +21,28 @@ (is (= "/foo/bar/baz/asdf" (path/path-join "/foo/bar//baz/asdf/quux/.."))) (is (= "assets:///foo.bar/baz" (path/path-join "assets:///foo.bar" "baz"))) (is (= "assets:///foo.bar/baz" (path/path-join "assets:///foo.bar/" "baz"))) + (is (= "file:///D:/a.txt" (path/path-join "file://" "D:/a.txt"))) + (is (= "file:///c:/x" (path/path-join "file://" "c:/x"))) (is (= "//NAS/MyGraph/logseq/config.edn" (path/path-join "//NAS/MyGraph" "logseq/config.edn"))))) (deftest prepend-protocol (testing "prepend-protocol" (is (= "file:///home/logseq/graph" (path/prepend-protocol "file:" "/home/logseq/graph"))) - (is (= "file:///C%3A/Graph/pages" (path/prepend-protocol "file:" "C:/Graph/pages"))) + (is (= "file:///C:/Graph/pages" (path/prepend-protocol "file:" "C:/Graph/pages"))) (is (= "file://NAS/MyGraph" (path/prepend-protocol "file:" "//NAS/MyGraph")) "Windows UNC URL"))) +(deftest file-url-or-path->path + (testing "file URL paths" + (is (= "D:/a.txt" (path/file-url-or-path->path "file:///D:/a.txt"))) + (is (= "c:/x" (path/file-url-or-path->path "file:///c:/x"))) + (is (= "//nas/share/x" (path/file-url-or-path->path "file://NAS/share/x"))) + (is (= "D:/a.txt" (path/file-url-or-path->path "file:///D%3A/a.txt"))) + (is (= "/home/admin/a.txt" (path/file-url-or-path->path "file:///home/admin/a.txt"))) + (is (= "/D:/a.txt" (path/file-url-or-path->path "/D:/a.txt"))) + (is (= "/D:\\a.txt" (path/file-url-or-path->path "/D:\\a.txt"))) + (is (= "/home/admin/a.txt" (path/file-url-or-path->path "/home/admin/a.txt"))))) + (deftest path-absolute (testing "absolute" (is (true? (path/absolute? "D:\\sources\\sources.md"))) diff --git a/src/main/frontend/components/block.cljs b/src/main/frontend/components/block.cljs index 3bcf92a8a4..82fd5f13c6 100644 --- a/src/main/frontend/components/block.cljs +++ b/src/main/frontend/components/block.cljs @@ -1309,6 +1309,19 @@ (join (config/get-repo-dir (state/get-current-repo)) (config/get-local-asset-absolute-path path))))) +(defn- file-link-path->open-path + [file-path] + (let [file-path (path/file-url-or-path->path file-path) + file-path (if (and util/win32? + (string? file-path) + (re-find #"^/[A-Za-z]:(?:[/\\]|$)" file-path)) + (subs file-path 1) + file-path)] + (if (or (path/absolute? file-path) + (path/protocol-url? file-path)) + file-path + (relative-assets-path->absolute-path file-path)))) + (rum/defc audio-link [config url href _label metadata full_text] (if (common-config/local-relative-asset? href) @@ -1391,13 +1404,14 @@ (util/electron?) (let [path (cond (string/starts-with? s "file://") - (string/replace s "file://" "") + s (string/starts-with? s "/") s :else - (relative-assets-path->absolute-path s))] + (relative-assets-path->absolute-path s)) + path (file-link-path->open-path path)] (->elem :a (cond-> @@ -1432,8 +1446,8 @@ :else (let [href (string-of-url url) - [protocol path] (or (and (= "Complex" (first url)) [(:protocol (second url)) (:link (second url))]) - (and (= "File" (first url)) ["file" (second url)])) + [protocol _path] (or (and (= "Complex" (first url)) [(:protocol (second url)) (:link (second url))]) + (and (= "File" (first url)) ["file" (second url)])) config (cond-> config (not (string/blank? protocol)) (assoc :link-js-url (try (js/URL. href) @@ -1442,8 +1456,9 @@ (= protocol "file") (if (show-link? href full_text) (media-link config url href label metadata full_text) - (let [href* (if (util/electron?) - (relative-assets-path->absolute-path href) + (let [file-path (file-link-path->open-path href) + href* (if (util/electron?) + file-path href)] [:div.flex.flex-row.items-center (ui/icon "file" {:class "opacity-50"}) @@ -1452,7 +1467,7 @@ (cond-> (if (util/electron?) {:on-click (fn [e] (util/stop e) - (js/window.apis.openPath path)) + (js/window.apis.openPath file-path)) :data-href href*} {:href (path/path-join "file://" href*) :data-href href* From c37386e39333b47092a286db871bddd59205d7dc Mon Sep 17 00:00:00 2001 From: Victor239 <12621257+Victor239@users.noreply.github.com> Date: Mon, 16 Mar 2026 16:50:33 +0000 Subject: [PATCH 2/3] fix: firefox paste --- src/main/frontend/extensions/html_parser.cljs | 7 ++++++- src/test/frontend/handler/paste_test.cljs | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/main/frontend/extensions/html_parser.cljs b/src/main/frontend/extensions/html_parser.cljs index 1d058ba54e..1719388463 100644 --- a/src/main/frontend/extensions/html_parser.cljs +++ b/src/main/frontend/extensions/html_parser.cljs @@ -225,7 +225,12 @@ (wrapper tag result)) (string? x) - x + (if @*inside-pre? + x + ;; Normalize whitespace for non-pre content (fixes Firefox soft line breaks) + (-> x + (string/replace #"\n" " ") + (string/replace #"\s+" " "))) :else (println "hiccup->doc error: " x))) diff --git a/src/test/frontend/handler/paste_test.cljs b/src/test/frontend/handler/paste_test.cljs index 92487afda6..ce7a3b4766 100644 --- a/src/test/frontend/handler/paste_test.cljs +++ b/src/test/frontend/handler/paste_test.cljs @@ -2,6 +2,7 @@ (:require ["/frontend/utils" :as utils] [cljs.test :refer [deftest are is testing]] [frontend.commands :as commands] + [frontend.db :as db] [frontend.extensions.html-parser :as html-parser] [frontend.handler.editor :as editor-handler] [frontend.handler.paste :as paste-handler] @@ -223,3 +224,21 @@ #js {:clipboardData #js {:getData (constantly clipboard) :files files}})] (is (= files (js->clj @pasted-file))))))) + +(deftest-async editor-on-paste-firefox-html-with-line-breaks + (testing "Firefox paste with soft line breaks should not create unwanted line breaks" + (let [clipboard-html "

Delegated access\n management means you can select a subset of roles for a given project \nand allow the granted organization to self-manage those roles for their \nusers.

" + expected-paste "Delegated access management means you can select a subset of roles for a given project and allow the granted organization to self-manage those roles for their users."] + (p/with-redefs + [commands/delete-selection! (constantly nil) + commands/simple-insert! (fn [_input text] (p/resolved text)) + util/stop (constantly nil) + util/get-selected-text (constantly "") + state/get-current-page (constantly nil) + db/get-page-format (constantly :markdown)] + (p/let [result ((paste-handler/editor-on-paste! nil) + #js {:clipboardData #js {:getData (fn [type] + (if (= type "text/html") + clipboard-html + expected-paste))}})] + (is (= expected-paste result))))))) From 1d16c9b47eb9293609fb3977d094dc355e7cf95f Mon Sep 17 00:00:00 2001 From: rcmerci Date: Thu, 14 May 2026 19:57:37 +0800 Subject: [PATCH 3/3] fix(dev): db-worker-node build don't copy publicdir --- scripts/build-db-worker-node-bundle.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/build-db-worker-node-bundle.mjs b/scripts/build-db-worker-node-bundle.mjs index a3810c1281..28e428ff51 100644 --- a/scripts/build-db-worker-node-bundle.mjs +++ b/scripts/build-db-worker-node-bundle.mjs @@ -89,6 +89,7 @@ async function main() { await build({ configFile: false, + publicDir: false, logLevel: "error", build: { codeSplitting: false,