linked refs

This commit is contained in:
Tienson Qin
2025-12-28 12:56:30 +08:00
parent 36408416c9
commit e9c2269e34
7 changed files with 200 additions and 34 deletions

View File

@@ -1,4 +1,4 @@
{:paths ["src/publish-ssr" "src/main" "src/electron" "src/resources"]
{:paths ["src/main" "src/electron" "src/resources"]
:deps
{org.clojure/clojure {:mvn/version "1.11.1"}
rum/rum {:git/url "https://github.com/logseq/rum" ;; fork

View File

@@ -1,8 +1,5 @@
{
"name": "@logseq/publish",
"version": "1.0.0",
"private": true,
"devDependencies": {
"@logseq/nbb-logseq": "github:logseq/nbb-logseq#feat-db-v31"
}
"private": true
}

View File

@@ -391,6 +391,73 @@
(defn inline->nodes-seq [ctx items]
(mapcat #(inline->nodes ctx %) items))
(defn block-content-nodes [block ctx]
(let [raw (or (:block/content block)
(:block/title block)
(:block/name block)
"")
format (keyword (or (:block/format block) :markdown))
ctx (assoc ctx :format format)
ast (inline-ast raw format)
content (if (seq ast)
(inline->nodes-seq ctx ast)
(content->nodes raw (:uuid->title ctx) (:graph-uuid ctx)))]
(into [:span.block-text] content)))
(defn block-content-from-ref [ref ctx]
(let [raw (or (get ref "source_block_content") "")
format (keyword (or (get ref "source_block_format") "markdown"))
ctx (assoc ctx :format format)
ast (inline-ast raw format)
content (if (seq ast)
(inline->nodes-seq ctx ast)
(content->nodes raw (:uuid->title ctx) (:graph-uuid ctx)))]
(into [:span.block-text] content)))
(defn ref-eid [value]
(cond
(number? value) value
(map? value) (:db/id value)
:else nil))
(defn refs-contain? [refs target-eid]
(when refs
(some #(= target-eid (ref-eid %))
(if (sequential? refs) refs [refs]))))
(defn page-refs-from-payload [payload page-eid page-uuid page-title graph-uuid]
(let [entities (datoms->entities (:datoms payload))
refs (->> entities
(mapcat (fn [[_e entity]]
(when (and (= (:block/page entity) page-eid)
(not= (:block/uuid entity) page-uuid))
(let [block-uuid (some-> (:block/uuid entity) str)
block-content (or (:block/content entity)
(:block/title entity)
(:block/name entity)
"")
block-format (name (or (:block/format entity) :markdown))
refs (:block/refs entity)
refs (if (sequential? refs) refs (when refs [refs]))
targets (->> refs
(map ref-eid)
(keep #(get entities %))
(keep :block/uuid)
(map str)
distinct)]
(when (seq targets)
(map (fn [target]
{:graph_uuid graph-uuid
:target_page_uuid target
:source_page_uuid (str page-uuid)
:source_page_title page-title
:source_block_uuid block-uuid
:source_block_content block-content
:source_block_format block-format
:updated_at (.now js/Date)})
targets)))))))]
(vec refs)))
(defn render-hiccup [node]
(cond
(nil? node) ""
@@ -433,32 +500,40 @@
(when (seq children)
[:ul.blocks
(map (fn [block]
(let [raw (or (:block/content block)
(:block/title block)
(:block/name block)
"")
format (keyword (or (:block/format block) :markdown))
ctx (assoc ctx :format format)
ast (inline-ast raw format)
content (if (seq ast)
(inline->nodes-seq ctx ast)
(content->nodes raw (:uuid->title ctx) (:graph-uuid ctx)))
child-id (:db/id block)
(let [child-id (:db/id block)
nested (render-block-tree children-by-parent child-id ctx)
has-children? (boolean nested)]
[:li.block
[:div.block-content
(into [:span.block-text] content)
(when has-children?
[:button.block-toggle
{:type "button" :aria-expanded "true"}
"▾"])]
"▾"])
(block-content-nodes block ctx)]
(when nested
[:div.block-children nested])]))
(sort-blocks children))])))
(defn linked-references
[ctx graph-uuid linked-by-page]
[:section.linked-refs
[:h2 "Linked references"]
(for [{:keys [page_uuid page_title blocks]} linked-by-page]
(let [ref-page-uuid page_uuid
ref-page-title page_title
href (when (and graph-uuid ref-page-uuid)
(str "/p/" graph-uuid "/" ref-page-uuid))]
[:div.ref-page
(if href
[:a.page-ref {:href href} ref-page-title]
[:div.ref-title ref-page-title])
(when (seq blocks)
[:ul.ref-blocks
(for [block blocks]
[:li.ref-block [:div.block-content (block-content-from-ref block ctx)]])])]))])
(defn render-page-html
[transit page_uuid-str]
[transit page_uuid-str refs-data]
(let [payload (read-transit-safe transit)
meta (get-publish-meta payload)
graph-uuid (when meta
@@ -511,6 +586,17 @@
:name->uuid name->uuid
:graph-uuid graph-uuid}
blocks (render-block-tree children-by-parent page-eid ctx)
linked-by-page (when refs-data
(->> (get refs-data "refs")
(group-by #(get % "source_page_uuid"))
(map (fn [[_ items]]
{:page_title (get (first items) "source_page_title")
:page_uuid (get (first items) "source_page_uuid")
:blocks items}))
(sort-by (fn [{:keys [page_title]}]
(string/lower-case (or page_title ""))))))
linked-refs (when (seq linked-by-page)
(linked-references ctx graph-uuid linked-by-page))
doc [:html
[:head
[:meta {:charset "utf-8"}]
@@ -532,6 +618,11 @@
".block-toggle:focus{outline:2px solid #c7b38f;outline-offset:2px;border-radius:4px;}"
".block-children{margin-left:16px;}"
".block.is-collapsed > .block-children { display: none; }"
".linked-refs{margin-top:36px;}"
".linked-refs h2{font-size:18px;margin:0 0 16px;color:#4b3b24;}"
".ref-page{margin:0 0 16px;}"
".ref-blocks{margin:8px 0 0 18px;padding:0;list-style:disc;}"
".ref-block{margin:6px 0;}"
".page-ref{color:#1a5fb4;text-decoration:none;}"
".page-ref:hover{text-decoration:underline;}"]]
[:body
@@ -542,7 +633,8 @@
{:type "button"
:onclick "window.toggleTopBlocks(this)"}
"Collapse all"]]
(when blocks blocks)]
(when blocks blocks)
(when linked-refs linked-refs)]
[:script
"document.addEventListener('click',function(e){var btn=e.target.closest('.block-toggle');if(!btn)return;var li=btn.closest('li.block');if(!li)return;var collapsed=li.classList.toggle('is-collapsed');btn.setAttribute('aria-expanded',String(!collapsed));});"
"window.toggleTopBlocks=function(btn){var list=document.querySelector('.blocks');if(!list){return;}var collapsed=list.classList.toggle('collapsed-all');list.querySelectorAll(':scope > .block').forEach(function(el){if(collapsed){el.classList.add('is-collapsed');}else{el.classList.remove('is-collapsed');}});if(btn){btn.textContent=collapsed?'Expand all':'Collapse all';}};"]]]]
@@ -563,7 +655,17 @@
(js-await [body (.arrayBuffer request)]
(let [{:keys [content_hash content_length graph page_uuid schema_version block_count created_at] :as meta}
(or (parse-meta-header request)
(meta-from-body body))]
(meta-from-body body))
payload (read-transit-safe (.decode text-decoder body))
payload-entities (datoms->entities (:datoms payload))
page-eid (some (fn [[e entity]]
(when (= (:block/uuid entity) (uuid page_uuid))
e))
payload-entities)
page-title (when page-eid
(entity->title (get payload-entities page-eid)))
refs (when (and page-eid page-title)
(page-refs-from-payload payload page-eid page_uuid page-title graph))]
(cond
(not (valid-meta? meta))
(bad-request "missing publish metadata")
@@ -592,7 +694,8 @@
:r2_key r2-key
:owner_sub (aget claims "sub")
:created_at created_at
:updated_at (.now js/Date)})
:updated_at (.now js/Date)
:refs refs})
meta-resp (.fetch do-stub "https://publish/pages"
#js {:method "POST"
:headers #js {"content-type" "application/json"}
@@ -664,6 +767,22 @@
:etag etag}
200))))))))))
(defn handle-get-page-refs [request env]
(let [url (js/URL. (.-url request))
parts (string/split (.-pathname url) #"/")
graph-uuid (nth parts 2 nil)
page_uuid (nth parts 3 nil)]
(if (or (nil? graph-uuid) (nil? page_uuid))
(bad-request "missing graph uuid or page uuid")
(js-await [^js do-ns (aget env "PUBLISH_META_DO")
do-id (.idFromName do-ns "index")
do-stub (.get do-ns do-id)
refs-resp (.fetch do-stub (str "https://publish/pages/" graph-uuid "/" page_uuid "/refs"))]
(if-not (.-ok refs-resp)
(not-found)
(js-await [refs (.json refs-resp)]
(json-response (js->clj refs :keywordize-keys true) 200)))))))
(defn handle-list-pages [env]
(js-await [^js do-ns (aget env "PUBLISH_META_DO")
do-id (.idFromName do-ns "index")
@@ -753,13 +872,19 @@
(if-not (.-ok meta-resp)
(not-found)
(js-await [meta (.json meta-resp)
refs-resp (let [index-id (.idFromName do-ns "index")
index-stub (.get do-ns index-id)]
(.fetch index-stub (str "https://publish/pages/" graph-uuid "/" page_uuid "/refs")))
refs-json (when (and refs-resp (.-ok refs-resp))
(js-await [raw (.json refs-resp)]
(js->clj raw :keywordize-keys false)))
r2 (aget env "PUBLISH_R2")
object (.get r2 (aget meta "r2_key"))]
(if-not object
(json-response {:error "missing transit blob"} 404)
(js-await [buffer (.arrayBuffer object)
transit (.decode text-decoder buffer)
html (render-page-html transit page_uuid)]
html (render-page-html transit page_uuid refs-json)]
(js/Response.
html
#js {:headers (merge-headers
@@ -788,6 +913,7 @@
(cond
(= (count parts) 3) (handle-list-graph-pages request env)
(= (nth parts 4 nil) "transit") (handle-get-page-transit request env)
(= (nth parts 4 nil) "refs") (handle-get-page-refs request env)
:else (handle-get-page request env)))
(and (string/starts-with? path "/pages/") (= method "DELETE"))
@@ -821,7 +947,19 @@
"created_at INTEGER,"
"updated_at INTEGER,"
"PRIMARY KEY (graph_uuid, page_uuid)"
");"))))
");")))
(sql-exec sql
(str "CREATE TABLE IF NOT EXISTS page_refs ("
"graph_uuid TEXT NOT NULL,"
"target_page_uuid TEXT NOT NULL,"
"source_page_uuid TEXT NOT NULL,"
"source_page_title TEXT,"
"source_block_uuid TEXT,"
"source_block_content TEXT,"
"source_block_format TEXT,"
"updated_at INTEGER,"
"PRIMARY KEY (graph_uuid, target_page_uuid, source_block_uuid)"
");")))
(defn row->meta [row]
(let [data (js->clj row :keywordize-keys false)]
@@ -868,7 +1006,31 @@
(aget body "owner_sub")
(aget body "created_at")
(aget body "updated_at"))
(json-response {:ok true}))
(let [refs (aget body "refs")
graph-uuid (aget body "graph")
page-uuid (aget body "page_uuid")]
(when (and graph-uuid page-uuid)
(sql-exec sql
"DELETE FROM page_refs WHERE graph_uuid = ? AND source_page_uuid = ?;"
graph-uuid
page-uuid)
(when (seq refs)
(doseq [ref refs]
(sql-exec sql
(str "INSERT OR REPLACE INTO page_refs ("
"graph_uuid, target_page_uuid, source_page_uuid, "
"source_page_title, source_block_uuid, source_block_content, "
"source_block_format, updated_at"
") VALUES (?, ?, ?, ?, ?, ?, ?, ?);")
(aget ref "graph_uuid")
(aget ref "target_page_uuid")
(aget ref "source_page_uuid")
(aget ref "source_page_title")
(aget ref "source_block_uuid")
(aget ref "source_block_content")
(aget ref "source_block_format")
(aget ref "updated_at")))))
(json-response {:ok true})))
(= "GET" (.-method request))
(let [url (js/URL. (.-url request))
@@ -876,6 +1038,20 @@
graph-uuid (nth parts 2 nil)
page_uuid (nth parts 3 nil)]
(cond
(= (nth parts 4 nil) "refs")
(let [rows (get-sql-rows
(sql-exec sql
(str "SELECT graph_uuid, target_page_uuid, source_page_uuid, "
"source_page_title, source_block_uuid, source_block_content, "
"source_block_format, updated_at "
"FROM page_refs WHERE graph_uuid = ? AND target_page_uuid = ? "
"ORDER BY updated_at DESC;")
graph-uuid
page_uuid))]
(json-response {:refs (map (fn [row]
(js->clj row :keywordize-keys false))
rows)}))
(and graph-uuid page_uuid)
(let [rows (get-sql-rows
(sql-exec sql

View File

@@ -42,6 +42,5 @@ metadata in a Durable Object backed by SQLite.
- For local testing, run `wrangler dev` and use `deps/publish/worker/scripts/dev_test.sh`.
- If you switch schema versions, clear local DO state with
`deps/publish/worker/scripts/clear_dev_state.sh`.
- Build the SSR bundle with `clojure -M:cljs release publish-ssr` before running the worker.
- Build the worker bundle with `clojure -M:cljs release publish-worker` before running the worker.
- For dev, you can run `clojure -M:cljs watch publish-worker` in one terminal.

View File

@@ -86,7 +86,7 @@
"cljs:mobile-watch": "clojure -M:cljs watch mobile db-worker --config-merge \"{:output-dir \\\"./static/mobile/js\\\" :asset-path \\\"/static/mobile/js\\\" :release {:asset-path \\\"http://localhost\\\"}}\"",
"cljs:release-mobile": "clojure -M:cljs release mobile db-worker --config-merge \"{:output-dir \\\"./static/mobile/js\\\" :asset-path \\\"/static/mobile/js\\\" :release {:asset-path \\\"http://localhost\\\"}}\"",
"cljs:dev-watch": "clojure -M:cljs watch app db-worker inference-worker electron mobile",
"cljs:app-watch": "clojure -M:cljs watch app db-worker inference-worker",
"cljs:app-watch": "clojure -M:cljs watch app db-worker inference-worker publish-worker",
"cljs:electron-watch": "clojure -M:cljs watch app db-worker inference-worker electron --config-merge \"{:asset-path \\\"./js\\\"}\"",
"cljs:release": "clojure -M:cljs release app db-worker inference-worker publishing electron",
"cljs:release-electron": "clojure -M:cljs release app db-worker inference-worker electron --debug && clojure -M:cljs release publishing",

View File

@@ -1,7 +1,7 @@
;; shadow-cljs configuration
{:deps true
:nrepl {:port 8701}
:source-paths ["src/publish-ssr" "src/main" "src/electron" "src/resources"]
:source-paths ["src/main" "src/electron" "src/resources"]
;; :ssl {:password "logseq"}
@@ -219,11 +219,6 @@
:devtools {:enabled true}
:compiler-options {:optimizations :simple}}
:publish-ssr {:target :npm-module
:entries [frontend.publish.ssr]
:output-dir "deps/publish/worker/dist/ssr"
:compiler-options {:optimizations :simple}}
:publish-worker {:target :esm
:output-dir "deps/publish/worker/dist/worker"
:modules {:main {:exports {default logseq.publish.worker/worker

View File

@@ -98,7 +98,6 @@
(if-let [db* (and repo (db/get-db repo))]
(if (and page (:db/id page))
(let [payload (build-page-publish-datoms db* page)]
(notification/show! "Publishing page..." :success)
(-> (<post-publish! payload)
(p/then (fn [_resp]
(let [graph-uuid (some-> (ldb/get-graph-rtc-uuid db*) str)