From c97453d1dd70168aa0455c4032829d3401ff340d Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Tue, 30 Dec 2025 15:52:44 +0800 Subject: [PATCH] search --- .../src/logseq/publish/meta_store.cljs | 193 +++++++---- deps/publish/src/logseq/publish/publish.css | 176 ++++++++++ deps/publish/src/logseq/publish/publish.js | 302 ++++++++++++++++++ deps/publish/src/logseq/publish/render.cljs | 39 ++- deps/publish/src/logseq/publish/routes.cljs | 69 ++-- src/main/frontend/worker/publish.cljs | 52 ++- 6 files changed, 741 insertions(+), 90 deletions(-) diff --git a/deps/publish/src/logseq/publish/meta_store.cljs b/deps/publish/src/logseq/publish/meta_store.cljs index d248121438..2c8c8f827d 100644 --- a/deps/publish/src/logseq/publish/meta_store.cljs +++ b/deps/publish/src/logseq/publish/meta_store.cljs @@ -1,5 +1,6 @@ (ns logseq.publish.meta-store - (:require [clojure.string :as string] + (:require [cljs-bean.core :as bean] + [clojure.string :as string] [logseq.publish.common :as publish-common]) (:require-macros [logseq.publish.async :refer [js-await]])) @@ -71,6 +72,15 @@ "source_block_format TEXT," "updated_at INTEGER," "PRIMARY KEY (graph_uuid, tag_page_uuid, source_block_uuid)" + ");")) + (publish-common/sql-exec sql + (str "CREATE TABLE IF NOT EXISTS page_blocks (" + "graph_uuid TEXT NOT NULL," + "page_uuid TEXT NOT NULL," + "block_uuid TEXT NOT NULL," + "block_content TEXT," + "updated_at INTEGER," + "PRIMARY KEY (graph_uuid, block_uuid)" ");")))) (defn parse-page-tags [value] @@ -101,67 +111,71 @@ (cond (= "POST" (.-method request)) (js-await [body (.json request)] - (publish-common/sql-exec sql - (str "INSERT INTO pages (" - "page_uuid," - "page_title," - "page_tags," - "graph_uuid," - "schema_version," - "block_count," - "content_hash," - "content_length," - "r2_key," - "owner_sub," - "owner_username," - "created_at," - "updated_at," - "short_id," - "password_hash" - ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" - " ON CONFLICT(graph_uuid, page_uuid) DO UPDATE SET" - " page_uuid=excluded.page_uuid," - " page_title=excluded.page_title," - " page_tags=excluded.page_tags," - " schema_version=excluded.schema_version," - " block_count=excluded.block_count," - " content_hash=excluded.content_hash," - " content_length=excluded.content_length," - " r2_key=excluded.r2_key," - " owner_sub=excluded.owner_sub," - " owner_username=excluded.owner_username," - " updated_at=excluded.updated_at," - " short_id=excluded.short_id," - " password_hash=excluded.password_hash;") - (aget body "page_uuid") - (aget body "page_title") - (aget body "page_tags") - (aget body "graph") - (aget body "schema_version") - (aget body "block_count") - (aget body "content_hash") - (aget body "content_length") - (aget body "r2_key") - (aget body "owner_sub") - (aget body "owner_username") - (aget body "created_at") - (aget body "updated_at") - (aget body "short_id") - (aget body "password_hash")) - (let [refs (aget body "refs") - tagged-nodes (aget body "tagged_nodes") - graph-uuid (aget body "graph") - page-uuid (aget body "page_uuid")] - (when (and graph-uuid page-uuid) + (let [page-uuid (aget body "page_uuid") + graph-uuid (aget body "graph")] + (if (and (string? page-uuid) (string? graph-uuid)) (publish-common/sql-exec sql - "DELETE FROM page_refs WHERE graph_uuid = ? AND source_page_uuid = ?;" + (str "INSERT INTO pages (" + "page_uuid," + "page_title," + "page_tags," + "graph_uuid," + "schema_version," + "block_count," + "content_hash," + "content_length," + "r2_key," + "owner_sub," + "owner_username," + "created_at," + "updated_at," + "short_id," + "password_hash" + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + " ON CONFLICT(graph_uuid, page_uuid) DO UPDATE SET" + " page_uuid=excluded.page_uuid," + " page_title=excluded.page_title," + " page_tags=excluded.page_tags," + " schema_version=excluded.schema_version," + " block_count=excluded.block_count," + " content_hash=excluded.content_hash," + " content_length=excluded.content_length," + " r2_key=excluded.r2_key," + " owner_sub=excluded.owner_sub," + " owner_username=excluded.owner_username," + " updated_at=excluded.updated_at," + " short_id=excluded.short_id," + " password_hash=excluded.password_hash;") + page-uuid + (aget body "page_title") + (aget body "page_tags") graph-uuid - page-uuid) - (publish-common/sql-exec sql - "DELETE FROM page_tags WHERE graph_uuid = ? AND source_page_uuid = ?;" - graph-uuid - page-uuid) - (when (seq refs) + (aget body "schema_version") + (aget body "block_count") + (aget body "content_hash") + (aget body "content_length") + (aget body "r2_key") + (aget body "owner_sub") + (aget body "owner_username") + (aget body "created_at") + (aget body "updated_at") + (aget body "short_id") + (aget body "password_hash")) + (throw (js/Error. "publish: missing page_uuid or graph"))) + (let [refs (aget body "refs") + tagged-nodes (aget body "tagged_nodes") + blocks (aget body "blocks") + graph-uuid (aget body "graph") + page-uuid (aget body "page_uuid")] + (when (and graph-uuid page-uuid) + (publish-common/sql-exec sql + "DELETE FROM page_refs WHERE graph_uuid = ? AND source_page_uuid = ?;" + graph-uuid + page-uuid) + (publish-common/sql-exec sql + "DELETE FROM page_tags WHERE graph_uuid = ? AND source_page_uuid = ?;" + graph-uuid + page-uuid) (doseq [ref refs] (publish-common/sql-exec sql (str "INSERT OR REPLACE INTO page_refs (" @@ -178,8 +192,8 @@ (aget ref "source_block_uuid") (aget ref "source_block_content") (aget ref "source_block_format") - (aget ref "updated_at")))) - (when (seq tagged-nodes) + (aget ref "updated_at"))) + (doseq [tag tagged-nodes] (publish-common/sql-exec sql (str "INSERT OR REPLACE INTO page_tags (" @@ -195,7 +209,21 @@ (aget tag "source_block_uuid") (aget tag "source_block_content") (aget tag "source_block_format") - (aget tag "updated_at"))))) + (aget tag "updated_at")))) + (publish-common/sql-exec sql + "DELETE FROM page_blocks WHERE graph_uuid = ? AND page_uuid = ?;" + graph-uuid + page-uuid) + (doseq [block blocks] + (publish-common/sql-exec sql + (str "INSERT OR REPLACE INTO page_blocks (" + "graph_uuid, page_uuid, block_uuid, block_content, updated_at" + ") VALUES (?, ?, ?, ?, ?);") + (aget body "graph") + (aget block "page_uuid") + (aget block "block_uuid") + (aget block "block_content") + (aget block "updated_at")))) (publish-common/json-response {:ok true}))) (= "GET" (.-method request)) @@ -204,6 +232,45 @@ graph-uuid (nth parts 2 nil) page-uuid (nth parts 3 nil)] (cond + (= (nth parts 1 nil) "search") + (let [graph-uuid (nth parts 2 nil) + query (.get (.-searchParams url) "q") + query (some-> query string/trim) + query (when (and query (not (string/blank? query))) + (string/lower-case query))] + (if (or (string/blank? graph-uuid) (string/blank? query)) + (publish-common/bad-request "missing graph uuid or query") + (let [like-query (str "%" query "%") + pages (publish-common/get-sql-rows + (publish-common/sql-exec sql + (str "SELECT page_uuid, page_title, short_id " + "FROM pages " + "WHERE graph_uuid = ? " + "AND password_hash IS NULL " + "AND page_title IS NOT NULL " + "AND lower(page_title) LIKE ? " + "ORDER BY updated_at DESC " + "LIMIT 20;") + graph-uuid + like-query)) + blocks (publish-common/get-sql-rows + (publish-common/sql-exec sql + (str "SELECT page_blocks.page_uuid, page_blocks.block_uuid, " + "page_blocks.block_content, pages.page_title, pages.short_id " + "FROM page_blocks " + "LEFT JOIN pages " + "ON pages.graph_uuid = page_blocks.graph_uuid " + "AND pages.page_uuid = page_blocks.page_uuid " + "WHERE page_blocks.graph_uuid = ? " + "AND pages.password_hash IS NULL " + "AND page_blocks.block_content IS NOT NULL " + "AND lower(page_blocks.block_content) LIKE ? " + "ORDER BY page_blocks.updated_at DESC " + "LIMIT 50;") + graph-uuid + like-query))] + (publish-common/json-response {:pages pages :blocks blocks})))) + (= (nth parts 1 nil) "tag") (let [tag-name (when-let [raw (nth parts 2 nil)] (js/decodeURIComponent raw)) diff --git a/deps/publish/src/logseq/publish/publish.css b/deps/publish/src/logseq/publish/publish.css index 00d7e9e07c..09930fd623 100644 --- a/deps/publish/src/logseq/publish/publish.css +++ b/deps/publish/src/logseq/publish/publish.css @@ -142,6 +142,170 @@ a:hover { margin-right: auto; } +.publish-search { + position: relative; + display: flex; + align-items: center; + gap: 6px; + min-width: 240px; + flex-direction: row-reverse; + height: 32px; +} + +.publish-search-toggle { + border: none; + background: var(--button-bg); + color: var(--ink); + width: 32px; + height: 32px; + padding: 0; + border-radius: 999px; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + transition: background 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease; +} + +.publish-search-toggle:hover { + background: var(--button-bg-hover); + box-shadow: 0 6px 14px rgba(40, 39, 38, 0.12); + transform: translateY(-1px); +} + +.publish-search-toggle .ti { + font-size: 16px; +} + +.publish-search-input { + width: 0; + opacity: 0; + padding: 0 12px; + border: 1px solid var(--border); + background: var(--input-bg); + color: var(--ink); + font-size: 13px; + border-radius: 999px; + pointer-events: none; + transition: width 0.25s ease, opacity 0.2s ease, padding 0.25s ease; +} + +.publish-search.is-expanded .publish-search-input { + width: min(320px, 70vw); + padding: 8px 12px; + opacity: 1; + pointer-events: auto; +} + +.publish-search-input:focus { + outline: none; +} + +.publish-search-input:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.publish-search-results { + opacity: 0; + transform: translateY(-6px); + position: absolute; + right: 0; + top: calc(100% + 24px); + z-index: 10; + width: min(480px, 90vw); + max-height: 320px; + overflow: auto; + border-radius: 16px; + background: var(--surface-strong); + box-shadow: var(--shadow); + transition: opacity 0.2s ease, transform 0.2s ease; +} + +.publish-search.is-expanded .publish-search-results:not([hidden]) { + opacity: 1; + transform: translateY(0); +} + +.publish-search-hint { + position: absolute; + right: 40px; + top: calc(100% + 6px); + font-size: 10px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--muted); + opacity: 0; + pointer-events: none; + transition: opacity 0.2s ease; +} + +.publish-search.is-expanded .publish-search-hint { + opacity: 1; +} +.publish-search-list { + display: flex; + flex-direction: column; + gap: 2px; +} + +.publish-search-section { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.18em; + color: var(--muted); + margin: 6px 6px 2px; +} + +.publish-search-result { + background: transparent; + text-align: left; + display: grid; + padding: 4px 12px; + cursor: pointer; + color: var(--ink); + text-decoration: none; +} + +.publish-search-result:hover { + background: var(--surface); +} + +.publish-search-result:first-child { + padding-top: 12px; +} + +.publish-search-result:last-child { + padding-bottom: 12px; +} + +.publish-search-result.is-active { + background: var(--surface); +} + +.publish-search-kind { + font-size: 10px; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--muted); +} + +.publish-search-title { + font-size: 13px; + font-weight: 600; +} + +.publish-search-snippet { + font-size: 12px; + color: var(--muted); +} + +.publish-search-empty { + font-size: 12px; + color: var(--muted); + padding: 8px; +} + .toolbar-btn { background: transparent; border: none; @@ -671,6 +835,18 @@ a:hover { justify-content: flex-end; } + .publish-search { + min-width: 100%; + } + + .publish-search-input { + width: 0; + } + + .publish-search.is-expanded .publish-search-input { + width: 100%; + } + .block-children { margin-left: 12px; } diff --git a/deps/publish/src/logseq/publish/publish.js b/deps/publish/src/logseq/publish/publish.js index f55ffc1131..0ccffe7443 100644 --- a/deps/publish/src/logseq/publish/publish.js +++ b/deps/publish/src/logseq/publish/publish.js @@ -244,6 +244,13 @@ document.addEventListener("keydown", (event) => { return; } + if (sequenceKey === "t" && key === "t") { + resetSequence(); + window.toggleTheme(); + event.preventDefault(); + return; + } + if (key === "t") { sequenceKey = "t"; if (sequenceTimer) clearTimeout(sequenceTimer); @@ -261,6 +268,42 @@ document.addEventListener("click", (event) => { window.toggleTheme(); }); +const searchStateMap = new WeakMap(); + +const getSearchContainerState = () => { + const container = + document.querySelector(".publish-search.is-expanded") || + document.querySelector(".publish-search"); + if (!container) return null; + return searchStateMap.get(container) || null; +}; + +document.addEventListener("keydown", (event) => { + const isMod = event.metaKey || event.ctrlKey; + if (!isMod) return; + + const key = (event.key || "").toLowerCase(); + if (!key) return; + + const typingTarget = isTypingTarget(event.target); + if ( + typingTarget && + !event.target.classList?.contains("publish-search-input") + ) { + return; + } + + const state = getSearchContainerState(); + if (!state) return; + + if (key === "k") { + event.preventDefault(); + state.setExpanded(true); + state.focusInput(); + return; + } +}); + window.toggleTopBlocks = (btn) => { const list = document.querySelector(".blocks"); if (!list) return; @@ -350,6 +393,264 @@ const initTwitterEmbeds = () => { }); }; +const buildSnippet = (text, query) => { + const haystack = text.toLowerCase(); + const needle = query.toLowerCase(); + const idx = haystack.indexOf(needle); + if (idx < 0) return text.slice(0, 160); + const start = Math.max(0, idx - 48); + const end = Math.min(text.length, idx + needle.length + 48); + return text.slice(start, end).replace(/\s+/g, " ").trim(); +}; + +const initSearch = () => { + const containers = Array.from( + document.querySelectorAll(".publish-search") + ); + if (!containers.length) return; + + containers.forEach((container) => { + const graphUuid = container.dataset.graphUuid; + const input = container.querySelector(".publish-search-input"); + const toggleBtn = container.querySelector(".publish-search-toggle"); + const resultsEl = container.querySelector(".publish-search-results"); + if (!input || !resultsEl || !toggleBtn) return; + + let debounceTimer = null; + let activeController = null; + let activeIndex = -1; + let activeItems = []; + + const hideResults = () => { + resultsEl.hidden = true; + resultsEl.innerHTML = ""; + activeIndex = -1; + activeItems = []; + }; + + const renderSection = (title) => { + const header = document.createElement("div"); + header.className = "publish-search-section"; + header.textContent = title; + return header; + }; + + const renderResults = (query, data) => { + const pages = data?.pages || []; + const blocks = data?.blocks || []; + + if (!pages.length && !blocks.length) { + resultsEl.innerHTML = ""; + const empty = document.createElement("div"); + empty.className = "publish-search-empty"; + empty.textContent = `No results for "${query}".`; + resultsEl.appendChild(empty); + resultsEl.hidden = false; + activeIndex = -1; + activeItems = []; + return; + } + + const list = document.createElement("div"); + list.className = "publish-search-list"; + + if (pages.length) { + pages.forEach((page) => { + const title = page.page_title || page.page_uuid; + const href = `/page/${graphUuid}/${page.page_uuid}`; + const item = document.createElement("a"); + item.className = "publish-search-result"; + item.href = href; + + const kind = document.createElement("span"); + kind.className = "publish-search-kind"; + kind.textContent = "Page"; + + const titleEl = document.createElement("span"); + titleEl.className = "publish-search-title"; + titleEl.textContent = title; + + item.appendChild(kind); + item.appendChild(titleEl); + list.appendChild(item); + }); + } + + if (blocks.length) { + blocks.forEach((block) => { + const title = block.page_title || block.page_uuid; + const href = `/page/${graphUuid}/${block.page_uuid}#block-${block.block_uuid}`; + const snippet = buildSnippet(block.block_content || "", query); + const item = document.createElement("a"); + item.className = "publish-search-result"; + item.href = href; + + const titleEl = document.createElement("span"); + titleEl.className = "publish-search-title"; + titleEl.textContent = title; + + const snippetEl = document.createElement("span"); + snippetEl.className = "publish-search-snippet"; + snippetEl.textContent = snippet; + + item.appendChild(titleEl); + item.appendChild(snippetEl); + list.appendChild(item); + }); + } + + resultsEl.innerHTML = ""; + resultsEl.appendChild(list); + resultsEl.hidden = false; + activeIndex = -1; + activeItems = Array.from( + resultsEl.querySelectorAll(".publish-search-result") + ); + activeItems.forEach((item, index) => { + item.addEventListener("mouseenter", () => { + activeIndex = index; + updateActive(); + }); + }); + }; + + const updateActive = () => { + if (!activeItems.length) return; + activeItems.forEach((item, index) => { + item.classList.toggle("is-active", index === activeIndex); + }); + const activeEl = activeItems[activeIndex]; + if (activeEl) { + activeEl.scrollIntoView({ block: "nearest" }); + } + }; + + const moveActive = (direction) => { + if (!activeItems.length) { + activeItems = Array.from( + resultsEl.querySelectorAll(".publish-search-result") + ); + } + if (!activeItems.length) return; + + if (activeIndex === -1) { + activeIndex = direction > 0 ? 0 : activeItems.length - 1; + } else { + activeIndex = + (activeIndex + direction + activeItems.length) % + activeItems.length; + } + updateActive(); + }; + + const activateSelection = () => { + if (!activeItems.length) { + activeItems = Array.from( + resultsEl.querySelectorAll(".publish-search-result") + ); + } + if (!activeItems.length) return; + const item = + activeIndex >= 0 ? activeItems[activeIndex] : activeItems[0]; + if (item?.href) { + window.location.href = item.href; + } + }; + + const setExpanded = (expanded) => { + container.classList.toggle("is-expanded", expanded); + toggleBtn.setAttribute("aria-expanded", String(expanded)); + if (expanded) { + input.focus(); + } else { + input.value = ""; + hideResults(); + } + }; + + const runSearch = async (query) => { + if (!query) { + hideResults(); + return; + } + + if (activeController) activeController.abort(); + activeController = new AbortController(); + + try { + const resp = await fetch( + `/search/${encodeURIComponent(graphUuid)}?q=${encodeURIComponent(query)}`, + { signal: activeController.signal } + ); + if (!resp.ok) throw new Error("search request failed"); + const data = await resp.json(); + renderResults(query, data); + } catch (error) { + if (error?.name === "AbortError") return; + hideResults(); + } + }; + + if (graphUuid) { + input.addEventListener("input", () => { + const query = input.value.trim(); + if (debounceTimer) clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => runSearch(query), 250); + }); + } + + input.addEventListener("keydown", (event) => { + if (event.key === "Escape") { + setExpanded(false); + } + if (event.key === "Enter") { + if (!resultsEl.hidden && input.value.trim()) { + activateSelection(); + event.preventDefault(); + } + } + if ( + !resultsEl.hidden && + input.value.trim() && + resultsEl.querySelector(".publish-search-result") + ) { + const key = event.key; + if (key === "ArrowDown" || key === "Down") { + moveActive(1); + event.preventDefault(); + } else if (key === "ArrowUp" || key === "Up") { + moveActive(-1); + event.preventDefault(); + } else if ((event.metaKey || event.ctrlKey) && key === "n") { + moveActive(1); + event.preventDefault(); + } else if ((event.metaKey || event.ctrlKey) && key === "p") { + moveActive(-1); + event.preventDefault(); + } + } + }); + + document.addEventListener("click", (event) => { + if (!container.contains(event.target)) setExpanded(false); + }); + + toggleBtn.addEventListener("click", () => { + const expanded = container.classList.contains("is-expanded"); + setExpanded(!expanded); + }); + + searchStateMap.set(container, { + setExpanded, + focusInput: () => input.focus(), + moveActive, + activateSelection, + hasResults: () => !!resultsEl.querySelector(".publish-search-result"), + isExpanded: () => container.classList.contains("is-expanded"), + }); + }); +}; + const initPublish = () => { applyTheme(preferredTheme()); renderPropertyIcons(); @@ -358,6 +659,7 @@ const initPublish = () => { } initTwitterEmbeds(); + initSearch(); document.querySelectorAll(".math-block").forEach((el) => { const tex = el.textContent; diff --git a/deps/publish/src/logseq/publish/render.cljs b/deps/publish/src/logseq/publish/render.cljs index acb28dbcdd..725a002821 100644 --- a/deps/publish/src/logseq/publish/render.cljs +++ b/deps/publish/src/logseq/publish/render.cljs @@ -135,6 +135,30 @@ [& nodes] (into [:div.page-toolbar] nodes)) +(defn- search-node + [graph-uuid] + (let [graph-id (some-> graph-uuid str)] + [:div.publish-search {:data-graph-uuid graph-id} + [:button.publish-search-toggle + {:type "button" + :aria-label "Search" + :aria-expanded "false"} + [:span.ti.ti-search {:aria-hidden "true"}]] + [:input.publish-search-input + (cond-> + {:id "publish-search-input" + :type "search" + :placeholder "Search graph (Cmd+K)" + :autocomplete "off" + :spellcheck "false" + :aria-label "Search graph"} + (string/blank? (or graph-id "")) + (assoc :disabled true :placeholder "Search unavailable"))] + [:div.publish-search-hint "Up/Down to navigate"] + [:div.publish-search-results + {:id "publish-search-results" + :hidden true}]])) + (defn- theme-init-script [] [:script @@ -967,8 +991,12 @@ positioned-left (render-positioned-properties block-left ctx (:entities ctx) :block-left) positioned-right (render-positioned-properties block-right ctx (:entities ctx) :block-right) positioned-below (render-positioned-properties block-below ctx (:entities ctx) :block-below) - properties (render-properties properties ctx (:entities ctx))] + properties (render-properties properties ctx (:entities ctx)) + block-uuid (:block/uuid display-block) + block-uuid-str (some-> block-uuid str)] [:li.block + (cond-> {:data-block-uuid block-uuid-str} + block-uuid-str (assoc :id (str "block-" block-uuid-str))) [:div.block-content (when positioned-left positioned-left) (block-display-node display-block ctx depth) @@ -1191,6 +1219,7 @@ (toolbar-node (when graph-uuid [:a.toolbar-btn {:href (str "/graph/" graph-uuid)} "Home"]) + (search-node graph-uuid) (theme-toggle-node)) (page-title-node page-title (:logseq.property/icon page-entity)) @@ -1236,6 +1265,7 @@ [:body [:main.wrap (toolbar-node + (search-node graph-uuid) (theme-toggle-node)) [:h1 "Published pages"] (if (seq rows) @@ -1275,6 +1305,7 @@ [:body [:main.wrap (toolbar-node + (search-node nil) (theme-toggle-node)) [:h1 title] (if (seq rows) @@ -1300,6 +1331,7 @@ (toolbar-node (when graph-uuid [:a.toolbar-btn {:href (str "/graph/" graph-uuid)} "Home"]) + (search-node graph-uuid) (theme-toggle-node)) [:h1 title] [:p.tag-sub (str "Tag: " tag-uuid)] @@ -1321,6 +1353,7 @@ [:body [:main.wrap (toolbar-node + (search-node nil) (theme-toggle-node)) [:h1 title] [:p.tag-sub (str "Tag: " tag-name)] @@ -1353,6 +1386,7 @@ (toolbar-node (when graph-uuid [:a.toolbar-btn {:href (str "/graph/" graph-uuid)} "Home"]) + (search-node graph-uuid) (theme-toggle-node)) [:h1 title] [:p.tag-sub (str "Reference: " ref-name)] @@ -1384,6 +1418,7 @@ (toolbar-node (when graph-uuid [:a.toolbar-btn {:href (str "/graph/" graph-uuid)} "Home"]) + (search-node graph-uuid) (theme-toggle-node)) [:h1 title] [:p.tag-sub "This page hasn't been published yet."] @@ -1400,6 +1435,7 @@ (toolbar-node (when graph-uuid [:a.toolbar-btn {:href (str "/graph/" graph-uuid)} "Home"]) + (search-node graph-uuid) (theme-toggle-node)) [:div.password-card [:h1 title] @@ -1427,6 +1463,7 @@ [:body [:main.wrap (toolbar-node + (search-node nil) (theme-toggle-node)) [:div.not-found [:p.not-found-eyebrow "404"] diff --git a/deps/publish/src/logseq/publish/routes.cljs b/deps/publish/src/logseq/publish/routes.cljs index 57a006b373..0af8ef6cad 100644 --- a/deps/publish/src/logseq/publish/routes.cljs +++ b/deps/publish/src/logseq/publish/routes.cljs @@ -1,5 +1,6 @@ (ns logseq.publish.routes - (:require [clojure.string :as string] + (:require [cljs-bean.core :as bean] + [clojure.string :as string] [logseq.publish.assets :as publish-assets] [logseq.publish.common :as publish-common] [logseq.publish.index :as publish-index] @@ -68,6 +69,8 @@ (get payload "page-title") (when page-eid (publish-model/entity->title (get payload-entities page-eid)))) + blocks (or (:blocks payload) + (get payload "blocks")) page-password-hash (or (:page-password-hash payload) (get payload "page-password-hash")) refs (when (and page-eid page-title) @@ -98,28 +101,34 @@ short-id (publish-common/short-id-for-page graph-uuid page_uuid) owner-sub (:owner_sub meta) owner-username (:owner_username meta) + updated-at (.now js/Date) _ (when-not (and owner-sub owner-username) (throw (ex-info "owner sub or username is missing" {:owner-sub owner-sub :owner-username owner-username}))) - payload (clj->js {:page_uuid page_uuid - :page_title page-title - :page_tags (when page-tags - (js/JSON.stringify (clj->js page-tags))) - :password_hash page-password-hash - :graph graph-uuid - :schema_version schema_version - :block_count block_count - :content_hash content_hash - :content_length content_length - :r2_key r2-key - :owner_sub owner-sub - :owner_username owner-username - :created_at created_at - :updated_at (.now js/Date) - :short_id short-id - :refs refs - :tagged_nodes tagged-nodes}) + payload (bean/->js + {:page_uuid page_uuid + :page_title page-title + :page_tags (when page-tags + (js/JSON.stringify (clj->js page-tags))) + :password_hash page-password-hash + :graph graph-uuid + :schema_version schema_version + :block_count block_count + :content_hash content_hash + :content_length content_length + :r2_key r2-key + :owner_sub owner-sub + :owner_username owner-username + :created_at created_at + :updated_at updated-at + :short_id short-id + :refs refs + :tagged_nodes tagged-nodes + :blocks (when (seq blocks) + (map (fn [block] + (assoc block :updated_at updated-at)) + blocks))}) meta-resp (.fetch do-stub "https://publish/pages" #js {:method "POST" :headers #js {"content-type" "application/json"} @@ -301,6 +310,25 @@ (js-await [meta (.json meta-resp)] (publish-common/json-response (js->clj meta :keywordize-keys true) 200)))))) +(defn handle-graph-search [request env] + (let [url (js/URL. (.-url request)) + parts (string/split (.-pathname url) #"/") + graph-uuid (nth parts 2 nil) + query (.get (.-searchParams url) "q")] + (if (or (string/blank? graph-uuid) (string/blank? query)) + (publish-common/bad-request "missing graph uuid or query") + (js-await [^js do-ns (aget env "PUBLISH_META_DO") + do-id (.idFromName do-ns "index") + do-stub (.get do-ns do-id) + resp (.fetch do-stub + (str "https://publish/search/" graph-uuid + "?q=" (js/encodeURIComponent query)) + #js {:method "GET"})] + (if-not (.-ok resp) + (publish-common/not-found) + (js-await [data (.json resp)] + (publish-common/json-response (js->clj data :keywordize-keys true) 200))))))) + (defn handle-graph-html [graph-uuid env] (if-not graph-uuid (publish-common/bad-request "missing graph uuid") @@ -581,6 +609,9 @@ (and (= path "/pages") (= method "GET")) (handle-list-pages env) + (and (string/starts-with? path "/search/") (= method "GET")) + (handle-graph-search request env) + (and (string/starts-with? path "/graph/") (= method "GET")) (let [parts (string/split path #"/") graph-uuid (nth parts 2 nil)] diff --git a/src/main/frontend/worker/publish.cljs b/src/main/frontend/worker/publish.cljs index c675901c8e..757c28000f 100644 --- a/src/main/frontend/worker/publish.cljs +++ b/src/main/frontend/worker/publish.cljs @@ -1,11 +1,13 @@ (ns frontend.worker.publish "Publish" - (:require [datascript.core :as d] + (:require [clojure.string :as string] + [datascript.core :as d] [frontend.common.thread-api :refer [def-thread-api]] [frontend.worker.state :as worker-state] [logseq.common.util :as common-util] [logseq.db :as ldb] [logseq.db.common.entity-util :as common-entity-util] + [logseq.db.frontend.content :as db-content] [logseq.db.frontend.property :as db-property] [logseq.db.frontend.schema :as db-schema])) @@ -73,9 +75,47 @@ (defn- collect-publish-blocks [db entity] (if (common-entity-util/page? entity) - (ldb/get-page-blocks db (:db/id entity)) + (:block/_page entity) (ldb/get-block-and-children db (:block/uuid entity)))) +(def ^:private publish-search-max-length 4096) + +(defn- block-page-eid + [block] + (let [page (:block/page block)] + (cond + (map? page) (:db/id page) + (number? page) page + :else nil))) + +(defn- block-search-content + [block] + (let [raw-content (or (:block/content block) + (:block/title block) + (:block/name block) + "") + raw-content (string/trim raw-content)] + (when-not (string/blank? raw-content) + (let [content (db-content/recur-replace-uuid-in-block-title + (assoc block :block/title raw-content)) + content (if (> (count content) publish-search-max-length) + (subs content 0 publish-search-max-length) + content)] + (string/trim content))))) + +(defn- collect-search-blocks + [blocks page-eid page-uuid] + (->> blocks + (keep (fn [block] + (when (and (= (block-page-eid block) page-eid) + (not= (:db/id block) page-eid) + (not (:logseq.property/created-from-property block))) + (when-let [block-uuid (some-> (:block/uuid block) str)] + (when-let [content (block-search-content block)] + {:page_uuid (str page-uuid) + :block_uuid block-uuid + :block_content content}))))))) + (defn- collect-embedded-blocks [db blocks] (let [linked-eids (->> blocks @@ -109,11 +149,7 @@ (let [page-id (:db/id entity) blocks (collect-publish-blocks db entity) embedded-blocks (collect-embedded-blocks db blocks) - blocks (->> (concat blocks embedded-blocks) - (remove (comp nil? :db/id)) - (map (juxt :db/id identity)) - (into {}) - vals) + blocks (concat blocks embedded-blocks) block-eids (map :db/id blocks) ref-eids (->> blocks (mapcat :block/refs) @@ -164,6 +200,7 @@ refs (when graph-uuid (publish-refs-from-blocks db blocks entity graph-uuid)) tags (page-tags entity) + search-blocks (collect-search-blocks blocks (:db/id entity) (:block/uuid entity)) raw-datoms (->> (mapcat (fn [eid] (map (fn [d] [(:e d) (:a d) (:v d) (:tx d) (:added d)]) @@ -182,6 +219,7 @@ :schema-version (db-schema/schema-version->string db-schema/version) :refs refs :page-tags tags + :blocks search-blocks :datoms datoms})) (def-thread-api :thread-api/build-publish-page-payload