@lancc/cmark 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,55 @@
1
+ /**
2
+ * ws-prefs.js —— 按工作区隔离的插件偏好
3
+ *
4
+ * 为什么需要:
5
+ * 全局 ~/.cmark-config.json 只适合放「所有工作区共用」的东西(当前工作区指针、主题)。
6
+ * Git 集成开关、远程凭据这类跟具体笔记库绑定的信息,必须按工作区分开存,
7
+ * 否则换一个工作区还沿用上一个库的开关和 token,会出现「绑了 A 库却拿 B 库凭据推送」。
8
+ *
9
+ * 存哪:
10
+ * %LOCALAPPDATA%\znotes-data\<工作区哈希>\prefs.json
11
+ * 哈希算法与 git-service 的 git-config.json 完全一致(md5(小写路径) 前 12 位),
12
+ * 保证同一个工作区的各类配置落在同一个目录里。
13
+ * 注意:绝不写进笔记仓库(不进 .cmark,也不进 git)。
14
+ */
15
+ const fs = require('fs')
16
+ const os = require('os')
17
+ const path = require('path')
18
+ const crypto = require('crypto')
19
+
20
+ function hashOf (ws) {
21
+ // 与 git-service.js 保持一致:只做小写,不再做别的规范化,避免老配置目录变位置
22
+ return crypto.createHash('md5').update(String(ws || '').toLowerCase()).digest('hex').slice(0, 12)
23
+ }
24
+
25
+ function dirOf (ws) {
26
+ const base = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local')
27
+ return path.join(base, 'znotes-data', hashOf(ws))
28
+ }
29
+
30
+ function fileOf (ws) {
31
+ return path.join(dirOf(ws), 'prefs.json')
32
+ }
33
+
34
+ function read (ws) {
35
+ if (!ws) return {}
36
+ try {
37
+ return JSON.parse(fs.readFileSync(fileOf(ws), 'utf8')) || {}
38
+ } catch (e) {
39
+ return {}
40
+ }
41
+ }
42
+
43
+ function patch (ws, obj) {
44
+ if (!ws) return { ok: false, message: '工作区为空' }
45
+ const next = Object.assign({}, read(ws), obj || {})
46
+ try {
47
+ fs.mkdirSync(dirOf(ws), { recursive: true })
48
+ fs.writeFileSync(fileOf(ws), JSON.stringify(next, null, 2), 'utf8')
49
+ return { ok: true, data: next }
50
+ } catch (e) {
51
+ return { ok: false, message: '偏好保存失败:' + e.message }
52
+ }
53
+ }
54
+
55
+ module.exports = { hashOf, dirOf, fileOf, read, patch }
package/preload.js CHANGED
@@ -12,8 +12,22 @@
12
12
  *
13
13
  * 另加 window.__CMARK 作为本版本新增的控制面(配置读写、选目录、重扫)。
14
14
  */
15
+ // 宿主适配必须在一切之前:uTools 只注入 window.utools,ZTools 只注入 window.ztools,
16
+ // 补齐之后下面的代码才能统一用 window.ztools.xxx(两套 API 名字一致)
17
+ try { require('./preload/host').applyHostShim() } catch (e) {}
18
+
15
19
  const path = require('path')
20
+
21
+ /** 启动期模块挂载失败时记一笔,方便用户/开发者直接在控制台看原因 */
22
+ function bootError(label, err) {
23
+ try {
24
+ window.__CMARK_BOOT_ERRORS = (window.__CMARK_BOOT_ERRORS || []).concat(
25
+ label + ': ' + ((err && err.stack) || (err && err.message) || String(err))
26
+ )
27
+ } catch (e) {}
28
+ }
16
29
  const fs = require('fs')
30
+ const { exportDocCss, exportDocColors } = require('./preload/export-css')
17
31
  const os = require('os')
18
32
 
19
33
  const CFG_PATH = path.join(os.homedir(), '.cmark-config.json')
@@ -65,6 +79,86 @@ function saveConfig(patch) {
65
79
 
66
80
  let config = loadConfig()
67
81
 
82
+ /* ---------------- 主题:light / dark / system ----------------
83
+ * 用户在设置里选,配置存在 ~/.cmark-config.json 的 theme 字段。
84
+ * 这里在 document-start 阶段就把 data-theme 写到 <html>,
85
+ * 避免首帧先渲染浅色再跳深色。
86
+ */
87
+ const THEME_MODES = ['light', 'dark', 'system']
88
+
89
+ function themeMode () {
90
+ const m = (config || {}).theme
91
+ return THEME_MODES.indexOf(m) >= 0 ? m : 'system'
92
+ }
93
+
94
+ function resolveTheme (mode) {
95
+ if (mode === 'light' || mode === 'dark') return mode
96
+ try {
97
+ return typeof matchMedia === 'function' && matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
98
+ } catch (e) {
99
+ return 'light'
100
+ }
101
+ }
102
+
103
+ let themeRetryArmed = false
104
+
105
+ function applyTheme () {
106
+ const mode = themeMode()
107
+ const resolved = resolveTheme(mode)
108
+ let applied = false
109
+ try {
110
+ const el = document && document.documentElement
111
+ if (el) {
112
+ el.setAttribute('data-theme', resolved)
113
+ el.setAttribute('data-theme-mode', mode)
114
+ el.style.colorScheme = resolved
115
+ applied = true
116
+ }
117
+ } catch (e) {}
118
+ // document-start 阶段 <html> 可能还不存在(页面重载时尤其明显):
119
+ // 之前这里静默失败后不再重试,结果 data-theme 永远没写上,
120
+ // 界面回退浅色,而设置面板读配置仍显示「深色」——两边对不上。
121
+ if (!applied && !themeRetryArmed) {
122
+ themeRetryArmed = true
123
+ try {
124
+ if (document && typeof document.addEventListener === 'function') {
125
+ document.addEventListener('DOMContentLoaded', function () { themeRetryArmed = false; applyTheme() }, { once: true })
126
+ }
127
+ } catch (e) {}
128
+ setTimeout(function () { themeRetryArmed = false; try { applyTheme() } catch (e) {} }, 80)
129
+ }
130
+ try {
131
+ if (window && typeof window.dispatchEvent === 'function') {
132
+ window.dispatchEvent(new CustomEvent('cmark-theme', { detail: { mode: mode, resolved: resolved } }))
133
+ }
134
+ } catch (e) {}
135
+ return { mode: mode, resolved: resolved }
136
+ }
137
+
138
+ const themeApi = {
139
+ get: function () { return { mode: themeMode(), resolved: resolveTheme(themeMode()) } },
140
+ set: function (mode) {
141
+ if (THEME_MODES.indexOf(mode) < 0) mode = 'system'
142
+ config = saveConfig({ theme: mode })
143
+ return applyTheme()
144
+ },
145
+ apply: applyTheme
146
+ }
147
+
148
+ try {
149
+ if (window) window.__CMARK_THEME = themeApi
150
+ } catch (e) {}
151
+
152
+ applyTheme()
153
+
154
+ // 跟随系统时,系统深浅色切换要实时生效
155
+ try {
156
+ const mq = matchMedia('(prefers-color-scheme: dark)')
157
+ const onChange = function () { if (themeMode() === 'system') applyTheme() }
158
+ if (mq.addEventListener) mq.addEventListener('change', onChange)
159
+ else if (mq.addListener) mq.addListener(onChange)
160
+ } catch (e) {}
161
+
68
162
  // 工作区未设置或不存在时,退到 ~/Documents/CMark,不弹任何框
69
163
  let workspace = config.workspace
70
164
  if (!workspace || !fs.existsSync(workspace)) {
@@ -75,12 +169,48 @@ if (!workspace || !fs.existsSync(workspace)) {
75
169
  } catch (e) {}
76
170
  }
77
171
 
172
+ /* ---------------- Git 集成开关 ----------------
173
+ * 存配置文件而不是 localStorage:插件跑在 file:// origin 下,localStorage 可能直接抛 SecurityError
174
+ * (被 try/catch 静默吞掉,表现就是「开关点了没生效、状态读不到」)。
175
+ * 这里给渲染层与注入脚本一个同步读写的入口,set 时广播事件让各界面同步。
176
+ */
177
+ const wsPrefs = require('./preload/ws-prefs.js')
178
+
179
+ function currentWs () {
180
+ try {
181
+ if (config && config.workspace) return config.workspace
182
+ // workspace 在下方才声明;这里用 typeof 兜住 TDZ,读取时它通常已初始化
183
+ return typeof workspace === 'string' ? workspace : ''
184
+ } catch (e) { return '' }
185
+ }
186
+
187
+ const gitApi = {
188
+ // 返回 true / false / null(null = 该工作区还没设置过)
189
+ // 存在「按工作区」的 prefs.json 里:换笔记库不会沿用上一个库的开关
190
+ get: function () {
191
+ const p = wsPrefs.read(currentWs())
192
+ if (typeof p.gitEnabled === 'boolean') return p.gitEnabled
193
+ // 兼容上一版写在全局配置里的值
194
+ if (config && typeof config.gitEnabled === 'boolean') return config.gitEnabled
195
+ return null
196
+ },
197
+ set: function (on) {
198
+ wsPrefs.patch(currentWs(), { gitEnabled: !!on })
199
+ try {
200
+ if (window) window.dispatchEvent(new CustomEvent('cmark-git-changed', { detail: { enabled: !!on } }))
201
+ } catch (e) {}
202
+ return !!on
203
+ }
204
+ }
205
+ try { if (window) window.__CMARK_GIT = gitApi } catch (e) {}
206
+
78
207
  /* ---------------- 挂载数据层 ---------------- */
79
208
  // ndb.js 的第二个参数是"能读到 workspace 字段的 JSON 路径"
80
209
  try {
81
210
  require('./preload/ndb.js')(window, CFG_PATH)
82
211
  } catch (e) {
83
212
  console.error('[cmark] ndb 挂载失败', e)
213
+ bootError('ndb', e)
84
214
  }
85
215
 
86
216
  /* ---------------- 挂载版本控制 ---------------- */
@@ -88,6 +218,7 @@ try {
88
218
  require('./preload/git-service.js')(window, workspace)
89
219
  } catch (e) {
90
220
  console.error('[cmark] git 服务挂载失败', e)
221
+ bootError('git-service', e)
91
222
  }
92
223
 
93
224
  /* ---------------- 挂载导出/附件/剪贴板 ---------------- */
@@ -95,6 +226,7 @@ try {
95
226
  window.services = require('./preload/services.js')(window)
96
227
  } catch (e) {
97
228
  console.error('[cmark] services 挂载失败', e)
229
+ bootError('services', e)
98
230
  window.services = {}
99
231
  }
100
232
 
@@ -151,18 +283,35 @@ try {
151
283
  require('./preload/git-panel.js')(window, workspace)
152
284
  } catch (e) {
153
285
  console.error('[cmark] git-panel 挂载失败', e)
286
+ bootError('git-panel', e)
154
287
  }
155
288
 
289
+ // 每个模块单独 try:之前三个挤在一个 try 里,前面任一个抛错,设置面板就整块不挂载,
290
+ // 表现就是「点设置没反应」(uTools 里尤其容易踩到)。
291
+ try {
292
+ require('./preload/remote-setup.js')(window)
293
+ } catch (e) {
294
+ console.error('[cmark] remote-setup 挂载失败', e)
295
+ bootError('remote-setup', e)
296
+ }
297
+ try {
298
+ require('./preload/remote-wizard.js')(window)
299
+ } catch (e) {
300
+ console.error('[cmark] remote-wizard 挂载失败', e)
301
+ bootError('remote-wizard', e)
302
+ }
156
303
  try {
157
304
  require('./preload/settings-panel.js')(window)
158
305
  } catch (e) {
159
306
  console.error('[cmark] settings-panel 挂载失败', e)
307
+ bootError('settings-panel', e)
160
308
  }
161
309
 
162
310
  try {
163
311
  require('./preload/rename-note.js')(window, window.__znotesFindInstance)
164
312
  } catch (e) {
165
313
  console.error('[cmark] rename-note 挂载失败', e)
314
+ bootError('rename-note', e)
166
315
  }
167
316
 
168
317
  /* ---------------- 控制面 ---------------- */
@@ -223,7 +372,7 @@ window.__CMARK = {
223
372
  return this._writeNoteFile(dir, name, content, attachments || [], 'md', false)
224
373
  },
225
374
 
226
- writeHtmlFile(dir, name, html, attachments, withKatex, selfContained) {
375
+ writeHtmlFile(dir, name, html, attachments, withKatex, selfContained, theme) {
227
376
  let css = ''
228
377
  // 自包含快照(每个元素都内联了 computed style)不能再套 github-markdown.css,
229
378
  // 那套样式会覆盖内联结果,导出的东西又和编辑区不一样了
@@ -236,10 +385,10 @@ window.__CMARK = {
236
385
  } catch (e) {}
237
386
  }
238
387
  const katexLink = withKatex ? '<link rel="stylesheet" href="./katex.min.css">' : ''
388
+ // 导出跟随当前主题;selfContained 快照已内联深/浅色值,壳背景必须匹配,
389
+ // 否则深色文字配白底几乎不可读
239
390
  const full = '<!doctype html>\n<html>\n<head>\n<meta charset="utf-8"/>\n<title>' + (name || '') + '</title>\n' +
240
- katexLink + '\n<style>\nhtml, body { margin: 0; padding: 0; background-color: #ffffff; }\n' +
241
- '.markdown-body { box-sizing: border-box; min-width: 200px; max-width: 980px; margin: 0 auto; padding: 45px; }\n' +
242
- '@media (max-width: 767px) { .markdown-body { padding: 15px; } }\n' + css + '\n</style>\n</head>\n<body>\n' +
391
+ katexLink + '\n<style>\n' + exportDocCss(theme) + '\n' + css + '\n</style>\n</head>\n<body>\n' +
243
392
  '<article class="markdown-body">\n' + String(html || '').trim() + '\n</article>\n</body>\n</html>'
244
393
  const extra = Array.isArray(attachments) ? attachments.slice() : []
245
394
  if (withKatex) {
@@ -251,7 +400,7 @@ window.__CMARK = {
251
400
  },
252
401
 
253
402
  /** 用隐藏窗口渲染后截图/打印成 PDF(V1.0.0 的做法) */
254
- exportFile(type, html, css, cb, outDir) {
403
+ exportFile(type, html, css, cb, outDir, theme) {
255
404
  if (!window.ztools || typeof window.ztools.createBrowserWindow !== 'function') {
256
405
  cb(new Error('当前宿主没有 createBrowserWindow,无法导出 ' + type))
257
406
  return
@@ -278,7 +427,30 @@ window.__CMARK = {
278
427
  try {
279
428
  let buf
280
429
  await new Promise((r) => setTimeout(r, 50))
281
- if (css) await win.webContents.insertCSS(css)
430
+ // Electron 的 insertCSS 会插到 <head> 最前面,被 export-file/index.html
431
+ // 自带的 background:#fff 覆盖(深色导出曾经导出白底)。改成把 <style>
432
+ // 追加到 body 末尾 + 直接内联背景,一定压得住。
433
+ const colors = exportDocColors(theme)
434
+ await win.webContents.executeJavaScript(
435
+ '(() => {' +
436
+ // export-file/index.html 会加载 github-markdown.css(浅色配色),
437
+ // 它的 .markdown-body / td / blockquote 规则特异性比我们的高,
438
+ // 深色导出会出现「深底 + 深字」和「白底表格」。直接禁用它,
439
+ // 全部改用 exportDocCss 这套随主题的样式。
440
+ ' Array.from(document.querySelectorAll("link[rel=stylesheet]")).forEach(function (l) {' +
441
+ ' if (l.href && l.href.indexOf("github-markdown") >= 0) l.disabled = true;' +
442
+ ' });' +
443
+ ' const css = ' + JSON.stringify(exportDocCss(theme) + '\n' + (css || '')) + ';' +
444
+ ' const st = document.createElement("style");' +
445
+ ' st.id = "__cmark-export-theme";' +
446
+ ' st.textContent = css;' +
447
+ ' document.body.appendChild(st);' +
448
+ ' document.documentElement.style.background = ' + JSON.stringify(colors.bg) + ';' +
449
+ ' document.body.style.background = ' + JSON.stringify(colors.bg) + ';' +
450
+ ' document.body.style.color = ' + JSON.stringify(colors.fg) + ';' +
451
+ ' return "ok";' +
452
+ '})()'
453
+ )
282
454
  await win.webContents.executeJavaScript(
283
455
  'document.querySelector(".markdown-body").innerHTML = ' + JSON.stringify(String(html || ''))
284
456
  )
@@ -364,6 +536,85 @@ window.__CMARK = {
364
536
  })
365
537
  },
366
538
 
539
+ /**
540
+ * 工作区(仓库根)。
541
+ * 语义:工作区永远是 git 仓库根,笔记可以放在它下面的任意子目录(甚至就是根)。
542
+ * 没显式配置时回退到已探测的仓库根,再回退到当前笔记目录(兼容老数据)。
543
+ */
544
+ repoDir() {
545
+ if (config && config.repoDir) return config.repoDir
546
+ try {
547
+ const st = window.__znotesGit && window.__znotesGit.inspect ? window.__znotesGit.inspect() : null
548
+ if (st && st.repoRoot) return st.repoRoot
549
+ } catch (e) {}
550
+ return workspace
551
+ },
552
+
553
+ /** 修改工作区(仓库根):只校验可写,不搬任何文件 */
554
+ pickRepoDir() {
555
+ let dir
556
+ try {
557
+ dir = window.ztools.showOpenDialog({
558
+ title: '选择工作区目录(git 仓库根)',
559
+ defaultPath: this.repoDir(),
560
+ properties: ['openDirectory', 'createDirectory'],
561
+ })
562
+ } catch (e) {
563
+ return { ok: false, message: '打开目录选择器失败:' + e.message }
564
+ }
565
+ if (!dir) return { ok: false, canceled: true }
566
+ if (Array.isArray(dir)) dir = dir[0]
567
+ try {
568
+ const probe = path.join(dir, '.cmark-write-test')
569
+ fs.writeFileSync(probe, 'ok')
570
+ fs.unlinkSync(probe)
571
+ } catch (e) {
572
+ return { ok: false, message: '这个目录不可写,请换一个' }
573
+ }
574
+ config = saveConfig({ repoDir: dir })
575
+ // 原来的笔记目录不在新工作区里(或没设置)→ 直接落到工作区根
576
+ const ws = config.workspace || workspace
577
+ let rel = ''
578
+ try { rel = path.relative(dir, ws) } catch (e) { rel = '..' }
579
+ if (!ws || rel.startsWith('..') || path.isAbsolute(rel)) {
580
+ workspace = dir
581
+ config = saveConfig({ workspace: dir, initialized: true })
582
+ }
583
+ return { ok: true, repoDir: dir, workspace: workspace, needReload: true }
584
+ },
585
+
586
+ /** 选择笔记目录:必须落在工作区(仓库根)之内,可以为根本身 */
587
+ pickNotesDir() {
588
+ const root = this.repoDir()
589
+ let dir
590
+ try {
591
+ dir = window.ztools.showOpenDialog({
592
+ title: '选择笔记目录(工作区内的子目录)',
593
+ defaultPath: root || workspace,
594
+ properties: ['openDirectory', 'createDirectory'],
595
+ })
596
+ } catch (e) {
597
+ return { ok: false, message: '打开目录选择器失败:' + e.message }
598
+ }
599
+ if (!dir) return { ok: false, canceled: true }
600
+ if (Array.isArray(dir)) dir = dir[0]
601
+ let rel = ''
602
+ try { rel = path.relative(root || dir, dir) } catch (e) { rel = '..' }
603
+ if (rel.startsWith('..') || path.isAbsolute(rel)) {
604
+ return { ok: false, message: '笔记目录必须在工作区(' + (root || '未设置') + ')之内' }
605
+ }
606
+ try {
607
+ const probe = path.join(dir, '.cmark-write-test')
608
+ fs.writeFileSync(probe, 'ok')
609
+ fs.unlinkSync(probe)
610
+ } catch (e) {
611
+ return { ok: false, message: '这个目录不可写,请换一个' }
612
+ }
613
+ workspace = dir
614
+ config = saveConfig({ workspace: dir, initialized: true })
615
+ return { ok: true, workspace: dir, noteRel: rel, needReload: true }
616
+ },
617
+
367
618
  /** 更换工作区:只校验可写,不搬任何文件 */
368
619
  pickWorkspace() {
369
620
  let dir
@@ -1431,6 +1682,10 @@ try {
1431
1682
  * ZTools 工具接口(对应 plugin.json 里的 tools 段)
1432
1683
  * AI 侧通过这些工具读写笔记、把 Markdown 渲染成图片/PDF
1433
1684
  * ================================================================== */
1685
+ // MCP 工具注册(ZTools 专有)。
1686
+ // plugin.json 里的 tools 声明已移除:插件本身不需要 MCP —— 其他 AI 直接读写磁盘上的 .md 即可。
1687
+ // 代码保留,若将来要恢复 MCP,把 tools 声明加回 plugin.json 就能生效;
1688
+ // uTools 没有 registerTool,这里的 return 会让它在 uTools 下自动跳过。
1434
1689
  ;(function registerTools() {
1435
1690
  const z = window.ztools
1436
1691
  if (!z || typeof z.registerTool !== 'function') return