From dc22f6103b36bdf3734901b198f3c8edfbd6cd55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=A9?= Date: Mon, 9 Feb 2026 12:46:51 +0800 Subject: [PATCH 1/9] fix: enable gfm markdown rendering --- docker-entrypoint.sh | 2 +- selfhosted/.env.example | 2 +- server/deps.edn | 7 +++++-- server/src/mdbrain/markdown.clj | 25 +++++++++++++++++++------ server/test/mdbrain/markdown_test.clj | 20 ++++++++++++++++++++ 5 files changed, 46 insertions(+), 10 deletions(-) diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 021027b..861c29f 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,4 +1,4 @@ #!/usr/bin/env sh set -eu -exec java -Djava.awt.headless=true ${JAVA_OPTS:-} -jar /app/app.jar +exec java -Djava.awt.headless=true --enable-native-access=ALL-UNNAMED ${JAVA_OPTS:-} -jar /app/app.jar diff --git a/selfhosted/.env.example b/selfhosted/.env.example index b61ab85..95a97a8 100644 --- a/selfhosted/.env.example +++ b/selfhosted/.env.example @@ -1,6 +1,6 @@ ## Docker image # Pin to a version (recommended) for reproducible deployments. -mdbrain_IMAGE=ghcr.io/blackstorm/mdbrain:latest +MDBRAIN_IMAGE=ghcr.io/blackstorm/mdbrain:latest ## Data path (optional) # Base directory inside the container where mdbrain stores DB, secrets, and local storage. diff --git a/server/deps.edn b/server/deps.edn index 15b2927..f787359 100644 --- a/server/deps.edn +++ b/server/deps.edn @@ -35,8 +35,11 @@ selmer/selmer {:mvn/version "1.12.69"} ;; Markdown 解析 - org.commonmark/commonmark {:mvn/version "0.27.0"} - org.commonmark/commonmark-ext-yaml-front-matter {:mvn/version "0.27.0"} + org.commonmark/commonmark {:mvn/version "0.27.1"} + org.commonmark/commonmark-ext-yaml-front-matter {:mvn/version "0.27.1"} + org.commonmark/commonmark-ext-gfm-tables {:mvn/version "0.27.1"} + org.commonmark/commonmark-ext-task-list-items {:mvn/version "0.27.1"} + org.commonmark/commonmark-ext-gfm-strikethrough {:mvn/version "0.27.1"} ;; 日志 org.clojure/tools.logging {:mvn/version "1.3.0"} diff --git a/server/src/mdbrain/markdown.clj b/server/src/mdbrain/markdown.clj index b372099..e589691 100644 --- a/server/src/mdbrain/markdown.clj +++ b/server/src/mdbrain/markdown.clj @@ -8,22 +8,34 @@ (:import [org.commonmark.parser Parser] [org.commonmark.renderer.html HtmlRenderer] [org.commonmark.renderer.text TextContentRenderer] - [org.commonmark.node Heading Text] - [org.commonmark.ext.front.matter YamlFrontMatterExtension])) + [org.commonmark.node Heading] + [org.commonmark.ext.front.matter YamlFrontMatterExtension] + [org.commonmark.ext.gfm.tables TablesExtension] + [org.commonmark.ext.task.list.items TaskListItemsExtension] + [org.commonmark.ext.gfm.strikethrough StrikethroughExtension])) ;;; ============================================================ ;;; 1. CommonMark 解析器初始化 ;;; ============================================================ -(def ^:private extensions - "CommonMark 扩展列表(含 YAML front matter 支持)" - [(YamlFrontMatterExtension/create)]) +(def ^:private parser-extensions + "CommonMark 解析扩展列表(含 YAML front matter 与 GFM 支持)" + [(YamlFrontMatterExtension/create) + (TablesExtension/create) + (TaskListItemsExtension/create) + (StrikethroughExtension/create)]) + +(def ^:private renderer-extensions + "HTML 渲染扩展列表(排除 YAML front matter)" + [(TablesExtension/create) + (TaskListItemsExtension/create) + (StrikethroughExtension/create)]) (def ^:private parser "线程安全的 Markdown 解析器(单例) 配置 YAML front matter 扩展用于识别和跳过 front matter" (-> (Parser/builder) - (.extensions extensions) + (.extensions parser-extensions) (.build))) (def ^:private renderer @@ -31,6 +43,7 @@ 注意:不添加 YAML 扩展,这样 YamlFrontMatterBlock 节点将被忽略 配置 softbreak 为
以保留单行换行" (-> (HtmlRenderer/builder) + (.extensions renderer-extensions) (.softbreak "
\n") (.build))) diff --git a/server/test/mdbrain/markdown_test.clj b/server/test/mdbrain/markdown_test.clj index 844c072..e5a27d8 100644 --- a/server/test/mdbrain/markdown_test.clj +++ b/server/test/mdbrain/markdown_test.clj @@ -76,6 +76,26 @@ (is (str/includes? html "Some text")) (is (str/includes? html "More text"))))) +;;; 测试 1.6: GFM 扩展(tables / task list / strikethrough) +(deftest test-gfm-extensions + (testing "GFM table 渲染为 HTML table" + (let [content "| A | B |\n| --- | --- |\n| 1 | 2 |" + html (md/md->html content)] + (is (str/includes? html "")) + (is (str/includes? html "
")) + (is (str/includes? html "")))) + + (testing "GFM task list 渲染为 checkbox" + (let [content "- [x] Done\n- [ ] Todo" + html (md/md->html content)] + (is (str/includes? html "type=\"checkbox\"")) + (is (str/includes? html "checked")) + (is (str/includes? html "disabled")))) + + (testing "GFM strikethrough 渲染为 " + (let [html (md/md->html "~~gone~~")] + (is (str/includes? html "gone"))))) + ;;; 测试 2: 解析单个 Obsidian 链接 (deftest test-parse-obsidian-link (testing "解析简单链接 [[filename]]" From c994c1c807e4f7af0acbaf697f9563d080fe0f4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=A9?= Date: Mon, 9 Feb 2026 12:58:37 +0800 Subject: [PATCH 2/9] add: md tests --- .../test/\344\273\200\344\271\210\346\230\257 mdbrain.md" | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git "a/vaults/test/\344\273\200\344\271\210\346\230\257 mdbrain.md" "b/vaults/test/\344\273\200\344\271\210\346\230\257 mdbrain.md" index 44dd697..b5eb06e 100644 --- "a/vaults/test/\344\273\200\344\271\210\346\230\257 mdbrain.md" +++ "b/vaults/test/\344\273\200\344\271\210\346\230\257 mdbrain.md" @@ -14,4 +14,7 @@ Mdbrain 的同步策略默认是实时的,你也可以进行手动同步。 ## 下面发布你的数字花园 [[自托管方案]] [[使用 Docker compose 部署]] -[[数学公式测试]] \ No newline at end of file +[[数学公式测试]] +[[Table test]] +[[Strikethrough test]] +[[Task List test]] \ No newline at end of file From 8689c802efb3fc806e99053609bfafd06943f6f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=A9?= Date: Mon, 9 Feb 2026 13:24:20 +0800 Subject: [PATCH 3/9] fix: prevent console indexing --- server/resources/templates/console/base.html | 1 + server/resources/templates/console/init.html | 1 + server/resources/templates/console/login.html | 1 + .../resources/templates/console/vaults.html | 1 + server/src/mdbrain/core.clj | 3 +- server/src/mdbrain/middleware.clj | 9 ++++++ server/test/mdbrain/middleware_test.clj | 29 +++++++++++++++++++ .../templates/console_template_test.clj | 21 ++++++++++++++ 8 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 server/test/mdbrain/templates/console_template_test.clj diff --git a/server/resources/templates/console/base.html b/server/resources/templates/console/base.html index 1ed0a5f..7137ac8 100644 --- a/server/resources/templates/console/base.html +++ b/server/resources/templates/console/base.html @@ -3,6 +3,7 @@ + {% block title %}console - Mdbrain{% endblock %} diff --git a/server/resources/templates/console/init.html b/server/resources/templates/console/init.html index bb04e0b..7539a39 100644 --- a/server/resources/templates/console/init.html +++ b/server/resources/templates/console/init.html @@ -3,6 +3,7 @@ + Welcome - Mdbrain diff --git a/server/resources/templates/console/login.html b/server/resources/templates/console/login.html index 47bfa46..8628bd8 100644 --- a/server/resources/templates/console/login.html +++ b/server/resources/templates/console/login.html @@ -3,6 +3,7 @@ + Login - Mdbrain diff --git a/server/resources/templates/console/vaults.html b/server/resources/templates/console/vaults.html index 7715de3..81d1904 100644 --- a/server/resources/templates/console/vaults.html +++ b/server/resources/templates/console/vaults.html @@ -3,6 +3,7 @@ + Console Dashboard - Mdbrain diff --git a/server/src/mdbrain/core.clj b/server/src/mdbrain/core.clj index 7aeda35..8382fc9 100644 --- a/server/src/mdbrain/core.clj +++ b/server/src/mdbrain/core.clj @@ -71,7 +71,8 @@ (-> routes/console-app (middleware/wrap-middleware) (wrap-resource-with-context "/publics/console" "publics/console") - (wrap-resource-with-context "/publics/shared" "publics/shared")) + (wrap-resource-with-context "/publics/shared" "publics/shared") + (middleware/wrap-noindex)) {:port port :host host})] {:server server diff --git a/server/src/mdbrain/middleware.clj b/server/src/mdbrain/middleware.clj index 85c06a0..e2acedd 100644 --- a/server/src/mdbrain/middleware.clj +++ b/server/src/mdbrain/middleware.clj @@ -107,6 +107,15 @@ (handler request))] (update response :headers (fnil merge {}) cors-headers)))))) +(defn wrap-noindex + "Add noindex headers for Console responses." + [handler] + (fn [request] + (let [response (handler request)] + (if (map? response) + (update response :headers (fnil assoc {}) "X-Robots-Tag" "noindex, nofollow") + response)))) + (defn wrap-app-host-binding "Enforce Host -> vault binding for the App server (8080). - Missing/invalid Host: 400 diff --git a/server/test/mdbrain/middleware_test.clj b/server/test/mdbrain/middleware_test.clj index 1b02f86..b3e9f58 100644 --- a/server/test/mdbrain/middleware_test.clj +++ b/server/test/mdbrain/middleware_test.clj @@ -192,3 +192,32 @@ (mock/header "Origin" "http://example.com")) response (wrapped-handler request)] (is (= "*" (get-in response [:headers "Access-Control-Allow-Origin"])))))) + +;; X-Robots-Tag Middleware 测试 +(deftest test-wrap-noindex + (testing "Adds X-Robots-Tag header when headers exist" + (let [handler (middleware/wrap-noindex (fn [_] + {:status 200 + :headers {"Content-Type" "text/plain"} + :body "ok"})) + response (handler (mock/request :get "/console"))] + (is (= "noindex, nofollow" (get-in response [:headers "X-Robots-Tag"]))) + (is (= "text/plain" (get-in response [:headers "Content-Type"]))))) + + (testing "Adds X-Robots-Tag header when headers missing" + (let [handler (middleware/wrap-noindex (fn [_] {:status 200 :body "ok"})) + response (handler (mock/request :get "/console"))] + (is (= "noindex, nofollow" (get-in response [:headers "X-Robots-Tag"]))))) + + (testing "Overrides existing X-Robots-Tag header" + (let [handler (middleware/wrap-noindex (fn [_] + {:status 200 + :headers {"X-Robots-Tag" "index"} + :body "ok"})) + response (handler (mock/request :get "/console"))] + (is (= "noindex, nofollow" (get-in response [:headers "X-Robots-Tag"]))))) + + (testing "Non-map responses pass through unchanged" + (let [handler (middleware/wrap-noindex (fn [_] "stream-body")) + response (handler (mock/request :get "/console"))] + (is (= "stream-body" response))))) diff --git a/server/test/mdbrain/templates/console_template_test.clj b/server/test/mdbrain/templates/console_template_test.clj new file mode 100644 index 0000000..7fd2e3e --- /dev/null +++ b/server/test/mdbrain/templates/console_template_test.clj @@ -0,0 +1,21 @@ +(ns mdbrain.templates.console-template-test + (:require [clojure.test :refer [deftest is testing]] + [clojure.java.io :as io] + [clojure.string :as str])) + +(deftest test-console-base-template-noindex + (testing "Console base template includes noindex meta tag" + (let [resource (io/resource "templates/console/base.html") + html (slurp resource)] + (is (some? resource)) + (is (str/includes? html ""))))) + +(deftest test-console-page-templates-noindex + (testing "All full-page console templates include noindex meta tag" + (doseq [template ["templates/console/login.html" + "templates/console/init.html" + "templates/console/vaults.html"]] + (let [resource (io/resource template) + html (slurp resource)] + (is (some? resource)) + (is (str/includes? html "")))))) From 40214d336a251aaef983459dc30767595b2f53db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=A9?= Date: Mon, 9 Feb 2026 14:00:01 +0800 Subject: [PATCH 4/9] fix: align highlight.js theme with typography --- server/app.css | 12 +++ server/resources/publics/app/css/app.css | 8 ++ .../app/css/highlight-github-dark.min.css | 94 +++++++++++++++++++ server/resources/templates/app/base.html | 1 + .../mdbrain/templates/app_template_test.clj | 11 +++ vaults/test/Code block test.md | 13 +++ vaults/test/Math block test.md | 9 ++ vaults/test/Strikethrough test.md | 4 + vaults/test/Table test.md | 9 ++ vaults/test/Task List test.md | 6 ++ ...73\200\344\271\210\346\230\257 mdbrain.md" | 5 +- ...64\346\216\245\351\203\250\347\275\262.md" | 2 +- 12 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 server/resources/publics/app/css/highlight-github-dark.min.css create mode 100644 server/test/mdbrain/templates/app_template_test.clj create mode 100644 vaults/test/Code block test.md create mode 100644 vaults/test/Math block test.md create mode 100644 vaults/test/Strikethrough test.md create mode 100644 vaults/test/Table test.md create mode 100644 vaults/test/Task List test.md diff --git a/server/app.css b/server/app.css index 2686ae0..072545e 100644 --- a/server/app.css +++ b/server/app.css @@ -15,6 +15,18 @@ body { background-color: #fafafc; } +/* Keep prose in control of code block layout; highlight.js only adds colors. */ +.prose { + --tw-prose-pre-bg: #0d1117; + --tw-prose-pre-code: #c9d1d9; +} + +/* Align highlight.js with typography pre styles. */ +.prose pre code.hljs { + background: transparent; + padding: 0; +} + .navbar { position: fixed; top: 0; diff --git a/server/resources/publics/app/css/app.css b/server/resources/publics/app/css/app.css index 65da09b..5486e17 100644 --- a/server/resources/publics/app/css/app.css +++ b/server/resources/publics/app/css/app.css @@ -928,6 +928,14 @@ body { padding-top: var(--navbar-height); background-color: #fafafc; } +.prose { + --tw-prose-pre-bg: #0d1117; + --tw-prose-pre-code: #c9d1d9; +} +.prose pre code.hljs { + background: transparent; + padding: 0; +} .navbar { position: fixed; top: 0; diff --git a/server/resources/publics/app/css/highlight-github-dark.min.css b/server/resources/publics/app/css/highlight-github-dark.min.css new file mode 100644 index 0000000..d27432b --- /dev/null +++ b/server/resources/publics/app/css/highlight-github-dark.min.css @@ -0,0 +1,94 @@ +pre code.hljs { + display: block; + overflow-x: auto; + padding: 1em; +} +code.hljs { + padding: 3px 5px; +} /*! + Theme: GitHub Dark + Description: Dark theme as seen on github.com + Author: github.com + Maintainer: @Hirse + Updated: 2021-05-15 + + Outdated base version: https://github.com/primer/github-syntax-dark + Current colors taken from GitHub's CSS +*/ +.hljs { + color: #c9d1d9; + background: #0d1117; +} +.hljs-doctag, +.hljs-keyword, +.hljs-meta .hljs-keyword, +.hljs-template-tag, +.hljs-template-variable, +.hljs-type, +.hljs-variable.language_ { + color: #ff7b72; +} +.hljs-title, +.hljs-title.class_, +.hljs-title.class_.inherited__, +.hljs-title.function_ { + color: #d2a8ff; +} +.hljs-attr, +.hljs-attribute, +.hljs-literal, +.hljs-meta, +.hljs-number, +.hljs-operator, +.hljs-selector-attr, +.hljs-selector-class, +.hljs-selector-id, +.hljs-variable { + color: #79c0ff; +} +.hljs-meta .hljs-string, +.hljs-regexp, +.hljs-string { + color: #a5d6ff; +} +.hljs-built_in, +.hljs-symbol { + color: #ffa657; +} +.hljs-code, +.hljs-comment, +.hljs-formula { + color: #8b949e; +} +.hljs-name, +.hljs-quote, +.hljs-selector-pseudo, +.hljs-selector-tag { + color: #7ee787; +} +.hljs-subst { + color: #c9d1d9; +} +.hljs-section { + color: #1f6feb; + font-weight: 700; +} +.hljs-bullet { + color: #f2cc60; +} +.hljs-emphasis { + color: #c9d1d9; + font-style: italic; +} +.hljs-strong { + color: #c9d1d9; + font-weight: 700; +} +.hljs-addition { + color: #aff5b4; + background-color: #033a16; +} +.hljs-deletion { + color: #ffdcd7; + background-color: #67060c; +} diff --git a/server/resources/templates/app/base.html b/server/resources/templates/app/base.html index 6160c42..b83cb13 100644 --- a/server/resources/templates/app/base.html +++ b/server/resources/templates/app/base.html @@ -11,6 +11,7 @@ {% endif %} + {{ vault.custom-head-html|safe }} diff --git a/server/test/mdbrain/templates/app_template_test.clj b/server/test/mdbrain/templates/app_template_test.clj new file mode 100644 index 0000000..73e0f83 --- /dev/null +++ b/server/test/mdbrain/templates/app_template_test.clj @@ -0,0 +1,11 @@ +(ns mdbrain.templates.app-template-test + (:require [clojure.test :refer [deftest is testing]] + [clojure.java.io :as io] + [clojure.string :as str])) + +(deftest test-app-base-template-highlight-css + (testing "App base template includes highlight.js CSS" + (let [resource (io/resource "templates/app/base.html") + html (slurp resource)] + (is (some? resource)) + (is (str/includes? html "/publics/app/css/highlight-github-dark.min.css"))))) diff --git a/vaults/test/Code block test.md b/vaults/test/Code block test.md new file mode 100644 index 0000000..f21f230 --- /dev/null +++ b/vaults/test/Code block test.md @@ -0,0 +1,13 @@ +--- +mdbrain-id: 12b32bda-c20c-4284-9381-1d00119d0f41 +--- +## Json +```json +{ + "hello": "world" +} +``` +## JS +```js +const hello = "world" +``` \ No newline at end of file diff --git a/vaults/test/Math block test.md b/vaults/test/Math block test.md new file mode 100644 index 0000000..8c2bd4d --- /dev/null +++ b/vaults/test/Math block test.md @@ -0,0 +1,9 @@ +--- +mdbrain-id: efffa652-d067-44e3-8019-5d2fb54fbbb6 +--- +这是一个行内公式:$a^2 + b^2 = c^2$ +下方是一个数学公式 +$$ +a^2 + b^2 = c^2 +$$ +这是 $a^2$ 的平方 \ No newline at end of file diff --git a/vaults/test/Strikethrough test.md b/vaults/test/Strikethrough test.md new file mode 100644 index 0000000..274c176 --- /dev/null +++ b/vaults/test/Strikethrough test.md @@ -0,0 +1,4 @@ +--- +mdbrain-id: 3c9d6f60-cdcf-4daa-ae9d-f07005b5ad5a +--- + 这是 ~~被删除的内容~~,这是正常文本。 \ No newline at end of file diff --git a/vaults/test/Table test.md b/vaults/test/Table test.md new file mode 100644 index 0000000..bf0de58 --- /dev/null +++ b/vaults/test/Table test.md @@ -0,0 +1,9 @@ +--- +mdbrain-id: faf9c912-c425-4d01-9313-adccaea3f0bf +--- +## example + +| Header 1 | Header 2 | +|----------|----------| +| Cell A | Cell B | +| Cell C | Cell D | diff --git a/vaults/test/Task List test.md b/vaults/test/Task List test.md new file mode 100644 index 0000000..bc5f7e1 --- /dev/null +++ b/vaults/test/Task List test.md @@ -0,0 +1,6 @@ +--- +mdbrain-id: 280ef624-9e5e-460c-8e59-db0db5df9d5e +--- + - [x] 已完成任务 + - [ ] 未完成任务 + - [x] 另一个已完成 \ No newline at end of file diff --git "a/vaults/test/\344\273\200\344\271\210\346\230\257 mdbrain.md" "b/vaults/test/\344\273\200\344\271\210\346\230\257 mdbrain.md" index b5eb06e..04d05d7 100644 --- "a/vaults/test/\344\273\200\344\271\210\346\230\257 mdbrain.md" +++ "b/vaults/test/\344\273\200\344\271\210\346\230\257 mdbrain.md" @@ -14,7 +14,8 @@ Mdbrain 的同步策略默认是实时的,你也可以进行手动同步。 ## 下面发布你的数字花园 [[自托管方案]] [[使用 Docker compose 部署]] -[[数学公式测试]] +[[Math block test]] [[Table test]] [[Strikethrough test]] -[[Task List test]] \ No newline at end of file +[[Task List test]] +[[Code block test]] \ No newline at end of file diff --git "a/vaults/test/\347\233\264\346\216\245\351\203\250\347\275\262.md" "b/vaults/test/\347\233\264\346\216\245\351\203\250\347\275\262.md" index 361d6c1..3c8d6d4 100644 --- "a/vaults/test/\347\233\264\346\216\245\351\203\250\347\275\262.md" +++ "b/vaults/test/\347\233\264\346\216\245\351\203\250\347\275\262.md" @@ -3,4 +3,4 @@ mdbrain-id: 0a64fd08-4346-4bd7-acb6-159addfb1752 --- 直接部署的方案 [[什么是 Mdbrain]] -[[数学公式测试]] \ No newline at end of file +[[Math block test]] \ No newline at end of file From e4e9a075d73e480f0d9fe9e8c727b9ee440743c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=A9?= Date: Mon, 9 Feb 2026 14:35:44 +0800 Subject: [PATCH 5/9] fix: skip sync when publish config missing --- obsidian-plugin/src/main.test.ts | 59 ++++++++++++++----- obsidian-plugin/src/main.ts | 38 ++++++++++++ obsidian-plugin/src/plugin/settings-tab.ts | 21 ++++--- ...54\345\274\217\346\265\213\350\257\225.md" | 9 --- 4 files changed, 94 insertions(+), 33 deletions(-) delete mode 100644 "vaults/test/\346\225\260\345\255\246\345\205\254\345\274\217\346\265\213\350\257\225.md" diff --git a/obsidian-plugin/src/main.test.ts b/obsidian-plugin/src/main.test.ts index ce73b87..d923068 100644 --- a/obsidian-plugin/src/main.test.ts +++ b/obsidian-plugin/src/main.test.ts @@ -1,10 +1,13 @@ import type { App, PluginManifest } from "obsidian"; import { TFile } from "obsidian"; import { describe, expect, test, vi } from "vitest"; -import { DEFAULT_SETTINGS } from "./domain/types"; +import { DEFAULT_SETTINGS, type MdbrainSettings } from "./domain/types"; import MdbrainPlugin from "./main"; -const createPlugin = (appOverrides?: Partial) => { +const createPlugin = ( + appOverrides?: Partial, + settingsOverrides?: Partial, +) => { const app = { vault: { read: async () => "" }, metadataCache: { getFileCache: () => null }, @@ -21,7 +24,7 @@ const createPlugin = (appOverrides?: Partial) => { isDesktopOnly: false, }; const plugin = new MdbrainPlugin(app, manifest); - plugin.settings = { ...DEFAULT_SETTINGS }; + plugin.settings = { ...DEFAULT_SETTINGS, ...settingsOverrides }; return plugin; }; @@ -63,7 +66,7 @@ describe("MdbrainPlugin.handleAssetChange", () => { }); test("does not skip assets when reference index is not ready", async () => { - const plugin = createPlugin(); + const plugin = createPlugin(undefined, { publishKey: "test-key" }); const file = new TFile("assets/image.png", "image", "png"); const syncAssetFile = vi.fn().mockResolvedValue(true); @@ -82,7 +85,7 @@ describe("MdbrainPlugin.handleAssetChange", () => { }); test("syncs when asset is referenced", async () => { - const plugin = createPlugin(); + const plugin = createPlugin(undefined, { publishKey: "test-key" }); const file = new TFile("assets/image.png", "image", "png"); const syncAssetFile = vi.fn().mockResolvedValue(true); @@ -163,10 +166,13 @@ describe("MdbrainPlugin.collectReferencedAssetFiles", () => { describe("MdbrainPlugin.handleFileRename", () => { test("renames reference index and syncs note", async () => { - const plugin = createPlugin({ - metadataCache: { getFileCache: () => null } as never, - vault: { read: async () => "" } as never, - }); + const plugin = createPlugin( + { + metadataCache: { getFileCache: () => null } as never, + vault: { read: async () => "" } as never, + }, + { publishKey: "test-key" }, + ); const file = new TFile("notes/new.md"); const syncNoteFile = vi.fn().mockResolvedValue({ success: true, @@ -233,7 +239,7 @@ describe("MdbrainPlugin.handleMarkdownCacheChanged", () => { }, }; - const plugin = createPlugin({ vault, metadataCache }); + const plugin = createPlugin({ vault, metadataCache }, { publishKey: "test-key" }); const syncNote = vi.fn().mockResolvedValue({ success: true, @@ -287,7 +293,7 @@ describe("MdbrainPlugin.handleMarkdownCacheChanged", () => { }); test("uploads referenced assets after note sync", async () => { - const plugin = createPlugin(); + const plugin = createPlugin(undefined, { publishKey: "test-key" }); const note = new TFile("notes/a.md"); const assetA = new TFile("assets/a.png", "a", "png"); const assetB = new TFile("assets/b.png", "b", "png"); @@ -350,7 +356,7 @@ describe("MdbrainPlugin.handleMarkdownCacheChanged", () => { test("debounces multiple rapid cache changes and uses the latest content", async () => { vi.useFakeTimers(); - const plugin = createPlugin(); + const plugin = createPlugin(undefined, { publishKey: "test-key" }); const note = new TFile("notes/a.md"); const syncNoteFromCache = vi.fn().mockResolvedValue({ @@ -397,7 +403,7 @@ describe("MdbrainPlugin.handleMarkdownCacheChanged", () => { }); test("uploads linked notes after note sync", async () => { - const plugin = createPlugin(); + const plugin = createPlugin(undefined, { publishKey: "test-key" }); const note = new TFile("notes/a.md"); const linked = new TFile("notes/b.md"); const linkedMap = new Map([["note-b", linked]]); @@ -467,7 +473,7 @@ describe("MdbrainPlugin.handleMarkdownCacheChanged", () => { test("sync does not write to file (no self-write loop)", async () => { vi.useFakeTimers(); - const plugin = createPlugin(); + const plugin = createPlugin(undefined, { publishKey: "test-key" }); const note = new TFile("notes/a.md"); const originalContent = "# Test content"; @@ -515,7 +521,7 @@ describe("MdbrainPlugin.handleMarkdownCacheChanged", () => { test("skips sync when note has no client ID", async () => { vi.useFakeTimers(); - const plugin = createPlugin(); + const plugin = createPlugin(undefined, { publishKey: "test-key" }); const note = new TFile("notes/a.md"); const getClientId = vi.fn().mockResolvedValue(null); @@ -539,4 +545,27 @@ describe("MdbrainPlugin.handleMarkdownCacheChanged", () => { expect(syncNoteFromCache).not.toHaveBeenCalled(); vi.useRealTimers(); }); + + test("skips sync when publish config is missing", async () => { + vi.useFakeTimers(); + const plugin = createPlugin(); + const note = new TFile("notes/a.md"); + + const syncNoteFromCache = vi.fn(); + + const pluginAccess = plugin as unknown as { + syncNoteFromCache: (file: TFile, data: string, cache: unknown) => Promise; + settings: { autoSync: boolean }; + referenceIndex: { updateNote: (notePath: string) => void }; + }; + pluginAccess.syncNoteFromCache = syncNoteFromCache; + pluginAccess.settings.autoSync = true; + pluginAccess.referenceIndex = { updateNote: vi.fn() }; + + plugin.handleMarkdownCacheChanged(note, "content", null); + await vi.advanceTimersByTimeAsync(1200); + + expect(syncNoteFromCache).not.toHaveBeenCalled(); + vi.useRealTimers(); + }); }); diff --git a/obsidian-plugin/src/main.ts b/obsidian-plugin/src/main.ts index ce60b03..25f252e 100644 --- a/obsidian-plugin/src/main.ts +++ b/obsidian-plugin/src/main.ts @@ -32,6 +32,20 @@ export default class MdbrainPlugin extends Plugin { this.referenceIndexReady = false; } + private isSyncConfigured(): boolean { + const serverUrl = this.settings.serverUrl?.trim(); + const publishKey = this.settings.publishKey?.trim(); + return Boolean(serverUrl) && Boolean(publishKey); + } + + private ensureSyncConfigured(notify = false): boolean { + if (this.isSyncConfigured()) return true; + if (notify) { + new Notice("Publish is not configured. Set Publish URL and Key in Mdbrain settings."); + } + return false; + } + async onload() { console.log("[Mdbrain] Plugin loading (snapshot publish)..."); await this.loadSettings(); @@ -163,6 +177,9 @@ export default class MdbrainPlugin extends Plugin { } private async syncCurrentFile(file: TFile): Promise { + if (!this.ensureSyncConfigured(true)) { + return; + } const result = await this.syncNoteFile(file); if (!result.success) { new Notice("Publish failed: note upload failed"); @@ -177,6 +194,8 @@ export default class MdbrainPlugin extends Plugin { this.referenceIndex.updateNote(file.path, this.extractLinkpaths(data, cache)); + if (!this.isSyncConfigured()) return; + this.debounceService.debounce( file.path, async () => { @@ -201,6 +220,7 @@ export default class MdbrainPlugin extends Plugin { async handleFileDelete(_file: TFile) { if (!this.settings.autoSync) return; this.referenceIndex.removeNote(_file.path); + if (!this.isSyncConfigured()) return; await this.fullSync(); } @@ -211,6 +231,8 @@ export default class MdbrainPlugin extends Plugin { const content = cache ? "" : await this.app.vault.read(file); this.referenceIndex.updateNote(file.path, this.extractLinkpaths(content, cache)); + if (!this.isSyncConfigured()) return; + const result = await this.syncNoteFile(file); if (!result.success) { new Notice("Publish failed: note rename failed"); @@ -222,6 +244,7 @@ export default class MdbrainPlugin extends Plugin { async handleAssetChange(file: TFile) { if (!this.settings.autoSync) return; + if (!this.isSyncConfigured()) return; if (this.referenceIndexReady && !this.referenceIndex.isAssetReferenced(file.path)) { return; } @@ -246,6 +269,15 @@ export default class MdbrainPlugin extends Plugin { needUploadNotes: Array<{ id: string; hash: string }>; linkedNotesById: Map; }> { + if (!this.isSyncConfigured()) { + return { + success: false, + needUploadAssets: [], + assetsById: new Map(), + needUploadNotes: [], + linkedNotesById: new Map(), + }; + } const clientId = await getClientId(file, this.app); if (!clientId) { return { @@ -293,6 +325,9 @@ export default class MdbrainPlugin extends Plugin { } private async syncAssetFile(file: TFile): Promise { + if (!this.isSyncConfigured()) { + return false; + } const buffer = await this.app.vault.readBinary(file); const assetId = await hashString(file.path); const hash = await md5Hash(buffer); @@ -310,6 +345,9 @@ export default class MdbrainPlugin extends Plugin { } async fullSync(): Promise { + if (!this.ensureSyncConfigured(true)) { + return; + } const notes = await this.buildNoteSnapshot(); const referencedAssets = await this.collectReferencedAssetFiles(); const assets = await this.buildAssetSnapshot(referencedAssets); diff --git a/obsidian-plugin/src/plugin/settings-tab.ts b/obsidian-plugin/src/plugin/settings-tab.ts index 2f658de..39b2545 100644 --- a/obsidian-plugin/src/plugin/settings-tab.ts +++ b/obsidian-plugin/src/plugin/settings-tab.ts @@ -17,11 +17,10 @@ export class MdbrainSettingTab extends PluginSettingTab { new Setting(containerEl) .setName("Publish URL") - .setDesc("Your Mdbrain publish URL (must route /obsidian/* to Console)") + .setDesc("Your Mdbrain publish URL") .addText((text) => text - .setPlaceholder("https://notes.example.com") - .setValue(this.plugin.settings.serverUrl) + .setPlaceholder("https://console.example.com") .onChange(async (value) => { this.plugin.settings.serverUrl = value; await this.plugin.saveSettings(); @@ -34,7 +33,6 @@ export class MdbrainSettingTab extends PluginSettingTab { .addText((text) => text .setPlaceholder("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") - .setValue(this.plugin.settings.publishKey) .onChange(async (value) => { this.plugin.settings.publishKey = value; await this.plugin.saveSettings(); @@ -64,7 +62,10 @@ export class MdbrainSettingTab extends PluginSettingTab { 5000, ); } else { - new Notice(`Connection failed: ${result.error || "Unknown error"}`, 5000); + new Notice( + `Connection failed: ${result.error || "Unknown error"}`, + 5000, + ); } }), ); @@ -88,10 +89,12 @@ export class MdbrainSettingTab extends PluginSettingTab { .setName("Auto publish") .setDesc("Automatically publish on file changes") .addToggle((toggle) => - toggle.setValue(this.plugin.settings.autoSync).onChange(async (value) => { - this.plugin.settings.autoSync = value; - await this.plugin.saveSettings(); - }), + toggle + .setValue(this.plugin.settings.autoSync) + .onChange(async (value) => { + this.plugin.settings.autoSync = value; + await this.plugin.saveSettings(); + }), ); } } diff --git "a/vaults/test/\346\225\260\345\255\246\345\205\254\345\274\217\346\265\213\350\257\225.md" "b/vaults/test/\346\225\260\345\255\246\345\205\254\345\274\217\346\265\213\350\257\225.md" deleted file mode 100644 index 8c2bd4d..0000000 --- "a/vaults/test/\346\225\260\345\255\246\345\205\254\345\274\217\346\265\213\350\257\225.md" +++ /dev/null @@ -1,9 +0,0 @@ ---- -mdbrain-id: efffa652-d067-44e3-8019-5d2fb54fbbb6 ---- -这是一个行内公式:$a^2 + b^2 = c^2$ -下方是一个数学公式 -$$ -a^2 + b^2 = c^2 -$$ -这是 $a^2$ 的平方 \ No newline at end of file From 3bce19a9efa2ad533890b0cb939aa06bd3cd7294 Mon Sep 17 00:00:00 2001 From: Haoya Li Date: Sun, 22 Feb 2026 23:42:48 +0800 Subject: [PATCH 6/9] fix(plugin): prepopulate Publish URL in settings --- obsidian-plugin/src/plugin/settings-tab.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/obsidian-plugin/src/plugin/settings-tab.ts b/obsidian-plugin/src/plugin/settings-tab.ts index 39b2545..15093bd 100644 --- a/obsidian-plugin/src/plugin/settings-tab.ts +++ b/obsidian-plugin/src/plugin/settings-tab.ts @@ -21,6 +21,7 @@ export class MdbrainSettingTab extends PluginSettingTab { .addText((text) => text .setPlaceholder("https://console.example.com") + .setValue(this.plugin.settings.serverUrl ?? "") .onChange(async (value) => { this.plugin.settings.serverUrl = value; await this.plugin.saveSettings(); From bca45136c882862265100a2ef9b1ad5d489c334f Mon Sep 17 00:00:00 2001 From: Haoya Li Date: Tue, 24 Feb 2026 16:31:59 +0800 Subject: [PATCH 7/9] fix(db): correct wikilink LIKE filter and add test --- server/src/mdbrain/db.clj | 9 +++++++++ server/test/mdbrain/db_test.clj | 16 ++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/server/src/mdbrain/db.clj b/server/src/mdbrain/db.clj index 688421e..dedcd5f 100644 --- a/server/src/mdbrain/db.clj +++ b/server/src/mdbrain/db.clj @@ -265,6 +265,15 @@ (h/where [:and [:= :vault_id vault-id] [:is :deleted_at nil]])))) +(defn list-notes-with-wikilinks-by-vault + [vault-id] + (execute! + (-> (h/select :client_id :path :content) + (h/from :notes) + (h/where [:and [:= :vault_id vault-id] + [:is :deleted_at nil] + [:like :content "%[[%]]"]])))) + (defn get-note-by-path [vault-id path] (execute-one! (-> (h/select :*) diff --git a/server/test/mdbrain/db_test.clj b/server/test/mdbrain/db_test.clj index 2792651..7a03fc6 100644 --- a/server/test/mdbrain/db_test.clj +++ b/server/test/mdbrain/db_test.clj @@ -383,6 +383,22 @@ (is (= 2 (count results))) (is (every? #(contains? % :path) results))))) +(deftest test-list-notes-with-wikilinks-by-vault + (testing "Build query with wikilink LIKE pattern" + (let [vault-id (utils/generate-uuid) + captured-query (atom nil)] + (with-redefs [db/execute! (fn [query] + (reset! captured-query query) + [])] + (is (= [] (db/list-notes-with-wikilinks-by-vault vault-id))) + (is (= [:client_id :path :content] (:select @captured-query))) + (is (= [:notes] (:from @captured-query))) + (is (= [:and + [:= :vault_id vault-id] + [:is :deleted_at nil] + [:like :content "%[[%]]"]] + (:where @captured-query))))))) + (deftest test-get-note (testing "Get note by ID" (let [tenant-id (utils/generate-uuid) From e1129b62416889d2e7d75a580d641515bd7b2f82 Mon Sep 17 00:00:00 2001 From: Haoya Li Date: Tue, 24 Feb 2026 17:03:24 +0800 Subject: [PATCH 8/9] fix: format plugin settings tab for biome --- obsidian-plugin/src/plugin/settings-tab.ts | 25 ++++++++-------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/obsidian-plugin/src/plugin/settings-tab.ts b/obsidian-plugin/src/plugin/settings-tab.ts index 15093bd..e7d3458 100644 --- a/obsidian-plugin/src/plugin/settings-tab.ts +++ b/obsidian-plugin/src/plugin/settings-tab.ts @@ -32,12 +32,10 @@ export class MdbrainSettingTab extends PluginSettingTab { .setName("Publish Key") .setDesc("Publish Key from Mdbrain Console") .addText((text) => - text - .setPlaceholder("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") - .onChange(async (value) => { - this.plugin.settings.publishKey = value; - await this.plugin.saveSettings(); - }), + text.setPlaceholder("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx").onChange(async (value) => { + this.plugin.settings.publishKey = value; + await this.plugin.saveSettings(); + }), ); new Setting(containerEl) @@ -63,10 +61,7 @@ export class MdbrainSettingTab extends PluginSettingTab { 5000, ); } else { - new Notice( - `Connection failed: ${result.error || "Unknown error"}`, - 5000, - ); + new Notice(`Connection failed: ${result.error || "Unknown error"}`, 5000); } }), ); @@ -90,12 +85,10 @@ export class MdbrainSettingTab extends PluginSettingTab { .setName("Auto publish") .setDesc("Automatically publish on file changes") .addToggle((toggle) => - toggle - .setValue(this.plugin.settings.autoSync) - .onChange(async (value) => { - this.plugin.settings.autoSync = value; - await this.plugin.saveSettings(); - }), + toggle.setValue(this.plugin.settings.autoSync).onChange(async (value) => { + this.plugin.settings.autoSync = value; + await this.plugin.saveSettings(); + }), ); } } From 47baae40df8f8f3343e3d17c2c4ca0144e8dc772 Mon Sep 17 00:00:00 2001 From: Haoya Li Date: Tue, 24 Feb 2026 17:51:14 +0800 Subject: [PATCH 9/9] fix: match wikilinks anywhere in note content --- server/src/mdbrain/db.clj | 2 +- server/test/mdbrain/db_test.clj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/mdbrain/db.clj b/server/src/mdbrain/db.clj index dedcd5f..0d995c1 100644 --- a/server/src/mdbrain/db.clj +++ b/server/src/mdbrain/db.clj @@ -272,7 +272,7 @@ (h/from :notes) (h/where [:and [:= :vault_id vault-id] [:is :deleted_at nil] - [:like :content "%[[%]]"]])))) + [:like :content "%[[%]]%"]])))) (defn get-note-by-path [vault-id path] (execute-one! diff --git a/server/test/mdbrain/db_test.clj b/server/test/mdbrain/db_test.clj index 7a03fc6..d51691e 100644 --- a/server/test/mdbrain/db_test.clj +++ b/server/test/mdbrain/db_test.clj @@ -396,7 +396,7 @@ (is (= [:and [:= :vault_id vault-id] [:is :deleted_at nil] - [:like :content "%[[%]]"]] + [:like :content "%[[%]]%"]] (:where @captured-query))))))) (deftest test-get-note