finch-markdown-editor 0.1.0 → 0.1.2
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.
- package/dist/codemirror.js +29 -29
- package/dist/index.js +124 -18
- package/dist/panel.html +252 -15
- package/i18n/zh-CN.json +2 -2
- package/package.json +7 -3
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { mkdir, readFile, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import { watch } from "node:fs";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import path from "node:path";
|
|
@@ -101,6 +101,69 @@ async function writeStyleSlot(ctx, slot, value) {
|
|
|
101
101
|
await writeFile(styleSlotsFile(ctx), JSON.stringify(slots), "utf8");
|
|
102
102
|
return slots;
|
|
103
103
|
}
|
|
104
|
+
function draftPathFor(ctx, sourcePath) {
|
|
105
|
+
const digest = createHash("sha256").update(sourcePath).digest("hex").slice(0, 32);
|
|
106
|
+
return path.join(ctx.storagePath, "drafts", `${digest}.json`);
|
|
107
|
+
}
|
|
108
|
+
function hashText(text) {
|
|
109
|
+
return createHash("sha256").update(text).digest("hex");
|
|
110
|
+
}
|
|
111
|
+
async function readDraft(ctx, sourcePath) {
|
|
112
|
+
try {
|
|
113
|
+
const raw = JSON.parse(await readFile(draftPathFor(ctx, sourcePath), "utf8"));
|
|
114
|
+
return raw && raw.path === sourcePath && typeof raw.markdown === "string" ? raw : void 0;
|
|
115
|
+
} catch {
|
|
116
|
+
return void 0;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function writeDraft(ctx, sourcePath, markdown, base) {
|
|
120
|
+
try {
|
|
121
|
+
const file = draftPathFor(ctx, sourcePath);
|
|
122
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
123
|
+
const entry = { path: sourcePath, markdown, savedAt: Date.now() };
|
|
124
|
+
if (typeof base === "string") entry.baseHash = hashText(base);
|
|
125
|
+
await writeFile(file, JSON.stringify(entry), "utf8");
|
|
126
|
+
} catch (error) {
|
|
127
|
+
ctx.logger.warn(`Could not persist draft for ${sourcePath}: ${String(error)}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async function deleteDraft(ctx, sourcePath) {
|
|
131
|
+
await rm(draftPathFor(ctx, sourcePath), { force: true }).catch(() => {
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
var DRAFT_WRITE_DEBOUNCE_MS = 600;
|
|
135
|
+
var pendingDraftWrites = /* @__PURE__ */ new Map();
|
|
136
|
+
function scheduleDraftWrite(ctx, panelId, sourcePath, markdown, base) {
|
|
137
|
+
const existing = pendingDraftWrites.get(panelId);
|
|
138
|
+
if (existing?.timer) clearTimeout(existing.timer);
|
|
139
|
+
const entry = { path: sourcePath, markdown, base, timer: null };
|
|
140
|
+
entry.timer = setTimeout(() => {
|
|
141
|
+
entry.timer = null;
|
|
142
|
+
pendingDraftWrites.delete(panelId);
|
|
143
|
+
void writeDraft(ctx, sourcePath, markdown, base);
|
|
144
|
+
}, DRAFT_WRITE_DEBOUNCE_MS);
|
|
145
|
+
pendingDraftWrites.set(panelId, entry);
|
|
146
|
+
}
|
|
147
|
+
function flushPendingDraftWrite(ctx, panelId) {
|
|
148
|
+
const entry = pendingDraftWrites.get(panelId);
|
|
149
|
+
if (!entry) return;
|
|
150
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
151
|
+
pendingDraftWrites.delete(panelId);
|
|
152
|
+
void writeDraft(ctx, entry.path, entry.markdown, entry.base);
|
|
153
|
+
}
|
|
154
|
+
function cancelPendingDraftWrite(panelId) {
|
|
155
|
+
const entry = pendingDraftWrites.get(panelId);
|
|
156
|
+
if (entry?.timer) clearTimeout(entry.timer);
|
|
157
|
+
pendingDraftWrites.delete(panelId);
|
|
158
|
+
}
|
|
159
|
+
async function readFileWithDraft(ctx, sourcePath) {
|
|
160
|
+
const diskMarkdown = await readFile(sourcePath, "utf8");
|
|
161
|
+
const draft = await readDraft(ctx, sourcePath);
|
|
162
|
+
if (!draft || draft.markdown === diskMarkdown) return { markdown: diskMarkdown, diskMarkdown, draftRestored: false, draftConflict: false };
|
|
163
|
+
const diskMatchesBaseline = draft.baseHash === void 0 || draft.baseHash === hashText(diskMarkdown);
|
|
164
|
+
if (diskMatchesBaseline) return { markdown: draft.markdown, diskMarkdown, draftRestored: true, draftConflict: false };
|
|
165
|
+
return { markdown: diskMarkdown, diskMarkdown, draftRestored: false, draftConflict: true };
|
|
166
|
+
}
|
|
104
167
|
function sessionBucketKey(panel) {
|
|
105
168
|
return panel.sessionId || "__global__";
|
|
106
169
|
}
|
|
@@ -195,6 +258,18 @@ async function sendDocument(panel, state) {
|
|
|
195
258
|
livePanelDocuments.set(panel.id, state);
|
|
196
259
|
await panel.postMessage({ type: "document", ...state });
|
|
197
260
|
}
|
|
261
|
+
async function sendLiveDocument(ctx, panel, liveDocument) {
|
|
262
|
+
if (liveDocument.path && path.isAbsolute(liveDocument.path)) {
|
|
263
|
+
try {
|
|
264
|
+
const { markdown, diskMarkdown, draftRestored, draftConflict } = await readFileWithDraft(ctx, liveDocument.path);
|
|
265
|
+
await sendDocument(panel, { ...liveDocument, markdown, title: documentTitle(markdown, liveDocument.path), draftRestored, draftConflict, diskMarkdown });
|
|
266
|
+
return;
|
|
267
|
+
} catch (error) {
|
|
268
|
+
ctx.logger.warn(`Could not re-read ${liveDocument.path} for a reconnecting panel, resending cached copy: ${String(error)}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
await sendDocument(panel, liveDocument);
|
|
272
|
+
}
|
|
198
273
|
function payloadPath(panel) {
|
|
199
274
|
const payload = panel.payload;
|
|
200
275
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return void 0;
|
|
@@ -206,10 +281,10 @@ async function restoreDocument(ctx, panel) {
|
|
|
206
281
|
const sourcePath = await readLastPath(ctx, panel) ?? payloadPath(panel);
|
|
207
282
|
if (!sourcePath) return false;
|
|
208
283
|
try {
|
|
209
|
-
const markdown = await
|
|
284
|
+
const { markdown, diskMarkdown, draftRestored, draftConflict } = await readFileWithDraft(ctx, sourcePath);
|
|
210
285
|
watchSource(ctx, panel, sourcePath);
|
|
211
286
|
await rememberLastPath(ctx, panel, sourcePath);
|
|
212
|
-
await sendDocument(panel, { path: sourcePath, markdown, title: documentTitle(markdown, sourcePath) });
|
|
287
|
+
await sendDocument(panel, { path: sourcePath, markdown, title: documentTitle(markdown, sourcePath), draftRestored, draftConflict, diskMarkdown });
|
|
213
288
|
return true;
|
|
214
289
|
} catch (error) {
|
|
215
290
|
ctx.logger.warn(`Could not restore ${sourcePath}: ${String(error)}`);
|
|
@@ -318,6 +393,11 @@ function watchSource(ctx, panel, sourcePath) {
|
|
|
318
393
|
await sendDocument(panel, { path: sourcePath, markdown, title: documentTitle(markdown, sourcePath) });
|
|
319
394
|
} catch (error) {
|
|
320
395
|
ctx.logger.warn(`Source refresh failed: ${String(error)}`);
|
|
396
|
+
const code = error?.code;
|
|
397
|
+
if (code === "ENOENT") {
|
|
398
|
+
await panel.postMessage({ type: "sourceMissing", path: sourcePath }).catch(() => {
|
|
399
|
+
});
|
|
400
|
+
}
|
|
321
401
|
}
|
|
322
402
|
}, 150);
|
|
323
403
|
});
|
|
@@ -353,13 +433,19 @@ async function sendReady(ctx, panel) {
|
|
|
353
433
|
homeDir: os.homedir()
|
|
354
434
|
});
|
|
355
435
|
}
|
|
356
|
-
function revealInFileManager(ctx,
|
|
436
|
+
async function revealInFileManager(ctx, targetPath) {
|
|
357
437
|
try {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
438
|
+
const info = await stat(targetPath).catch(() => void 0);
|
|
439
|
+
const isFile = info?.isFile() ?? false;
|
|
440
|
+
if (process.platform === "darwin") {
|
|
441
|
+
spawn("open", isFile ? ["-R", targetPath] : [targetPath], { stdio: "ignore", detached: true }).unref();
|
|
442
|
+
} else if (process.platform === "win32") {
|
|
443
|
+
spawn("explorer", isFile ? [`/select,${targetPath}`] : [targetPath], { stdio: "ignore", detached: true }).unref();
|
|
444
|
+
} else {
|
|
445
|
+
spawn("xdg-open", [isFile ? path.dirname(targetPath) : targetPath], { stdio: "ignore", detached: true }).unref();
|
|
446
|
+
}
|
|
361
447
|
} catch (error) {
|
|
362
|
-
ctx.logger.warn(`Could not open file manager for ${
|
|
448
|
+
ctx.logger.warn(`Could not open file manager for ${targetPath}: ${String(error)}`);
|
|
363
449
|
}
|
|
364
450
|
}
|
|
365
451
|
async function handleMessage(ctx, panel, raw) {
|
|
@@ -373,7 +459,7 @@ async function handleMessage(ctx, panel, raw) {
|
|
|
373
459
|
await sendReady(ctx, panel);
|
|
374
460
|
const liveDocument = livePanelDocuments.get(panel.id);
|
|
375
461
|
if (liveDocument) {
|
|
376
|
-
await
|
|
462
|
+
await sendLiveDocument(ctx, panel, liveDocument);
|
|
377
463
|
} else if (!await restoreDocument(ctx, panel)) {
|
|
378
464
|
await panel.postMessage({ type: "lastFileUnavailable" });
|
|
379
465
|
}
|
|
@@ -421,10 +507,10 @@ async function handleMessage(ctx, panel, raw) {
|
|
|
421
507
|
return;
|
|
422
508
|
}
|
|
423
509
|
const sourcePath = picked.files[0].path;
|
|
424
|
-
const markdown = await
|
|
510
|
+
const { markdown, diskMarkdown, draftRestored, draftConflict } = await readFileWithDraft(ctx, sourcePath);
|
|
425
511
|
watchSource(ctx, panel, sourcePath);
|
|
426
512
|
await rememberLastPath(ctx, panel, sourcePath);
|
|
427
|
-
await sendDocument(panel, { path: sourcePath, markdown, title: documentTitle(markdown, sourcePath) });
|
|
513
|
+
await sendDocument(panel, { path: sourcePath, markdown, title: documentTitle(markdown, sourcePath), draftRestored, draftConflict, diskMarkdown });
|
|
428
514
|
} catch (error) {
|
|
429
515
|
ctx.logger.error(`pickFile() threw: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
|
|
430
516
|
await panel.postMessage({
|
|
@@ -442,10 +528,10 @@ async function handleMessage(ctx, panel, raw) {
|
|
|
442
528
|
return;
|
|
443
529
|
}
|
|
444
530
|
try {
|
|
445
|
-
const markdown = await
|
|
531
|
+
const { markdown, diskMarkdown, draftRestored, draftConflict } = await readFileWithDraft(ctx, sourcePath);
|
|
446
532
|
watchSource(ctx, panel, sourcePath);
|
|
447
533
|
await rememberLastPath(ctx, panel, sourcePath);
|
|
448
|
-
await sendDocument(panel, { path: sourcePath, markdown, title: documentTitle(markdown, sourcePath) });
|
|
534
|
+
await sendDocument(panel, { path: sourcePath, markdown, title: documentTitle(markdown, sourcePath), draftRestored, draftConflict, diskMarkdown });
|
|
449
535
|
} catch (error) {
|
|
450
536
|
await panel.postMessage({ type: "error", message: `Cannot read file: ${error instanceof Error ? error.message : String(error)}` });
|
|
451
537
|
}
|
|
@@ -462,7 +548,7 @@ async function handleMessage(ctx, panel, raw) {
|
|
|
462
548
|
case "requestLastFile": {
|
|
463
549
|
const liveDocument = livePanelDocuments.get(panel.id);
|
|
464
550
|
if (liveDocument) {
|
|
465
|
-
await
|
|
551
|
+
await sendLiveDocument(ctx, panel, liveDocument);
|
|
466
552
|
} else if (!await restoreDocument(ctx, panel)) {
|
|
467
553
|
await panel.postMessage({ type: "lastFileUnavailable" });
|
|
468
554
|
}
|
|
@@ -470,7 +556,7 @@ async function handleMessage(ctx, panel, raw) {
|
|
|
470
556
|
}
|
|
471
557
|
case "openPath": {
|
|
472
558
|
const targetPath = String(message.path ?? "").trim();
|
|
473
|
-
if (targetPath && path.isAbsolute(targetPath)) revealInFileManager(ctx, targetPath);
|
|
559
|
+
if (targetPath && path.isAbsolute(targetPath)) await revealInFileManager(ctx, targetPath);
|
|
474
560
|
return;
|
|
475
561
|
}
|
|
476
562
|
case "goHome": {
|
|
@@ -493,12 +579,28 @@ async function handleMessage(ctx, panel, raw) {
|
|
|
493
579
|
if (!path.isAbsolute(sourcePath)) return;
|
|
494
580
|
try {
|
|
495
581
|
await writeFile(sourcePath, String(message.markdown ?? ""), "utf8");
|
|
582
|
+
cancelPendingDraftWrite(panel.id);
|
|
583
|
+
await deleteDraft(ctx, sourcePath);
|
|
496
584
|
await panel.postMessage({ type: "savedMarkdown", path: sourcePath, requestId: message.requestId });
|
|
497
585
|
} catch (error) {
|
|
498
586
|
await panel.postMessage({ type: "error", message: `Could not save Markdown: ${error instanceof Error ? error.message : String(error)}` });
|
|
499
587
|
}
|
|
500
588
|
return;
|
|
501
589
|
}
|
|
590
|
+
case "saveDraft": {
|
|
591
|
+
const sourcePath = String(message.path ?? "").trim();
|
|
592
|
+
if (!path.isAbsolute(sourcePath)) return;
|
|
593
|
+
const base = typeof message.base === "string" ? message.base : void 0;
|
|
594
|
+
scheduleDraftWrite(ctx, panel.id, sourcePath, String(message.markdown ?? ""), base);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
case "discardDraft": {
|
|
598
|
+
const sourcePath = String(message.path ?? "").trim();
|
|
599
|
+
if (!path.isAbsolute(sourcePath)) return;
|
|
600
|
+
cancelPendingDraftWrite(panel.id);
|
|
601
|
+
await deleteDraft(ctx, sourcePath);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
502
604
|
case "renderBm": {
|
|
503
605
|
try {
|
|
504
606
|
const html = await renderWithBm(ctx, String(message.markdown ?? ""), String(message.markdownStyle ?? "kami"), message.customCss, () => {
|
|
@@ -637,6 +739,9 @@ function activate(ctx) {
|
|
|
637
739
|
},
|
|
638
740
|
"wechat-copy": {
|
|
639
741
|
svg: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"><path stroke-linecap="round" stroke-width="2" d="M7 7h.009m5.982 0H13m4.991 7.5H18m-4 0h.009"></path><path stroke-width="2" d="M10 16c0 2.761 2.686 5 6 5c.907 0 1.767-.168 2.538-.468c.189-.073.393-.1.592-.063L22 21l-.652-2.03a1.13 1.13 0 0 1 .11-.89A4.3 4.3 0 0 0 22 16c0-2.761-2.686-5-6-5s-6 2.239-6 5Z"></path><path stroke-width="2" d="M17.873 11.249Q18 10.639 18 10c0-3.866-3.582-7-8-7s-8 3.134-8 7c0 1.112.297 2.164.824 3.098c.147.26.196.567.108.853L2 17l3.914-.76c.208-.041.422-.013.617.07a9 9 0 0 0 3.589.69"></path></svg>'
|
|
742
|
+
},
|
|
743
|
+
feather: {
|
|
744
|
+
svg: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.086 18.412A2 2 0 0 1 12.67 19H5v-7.672a2 2 0 0 1 .586-1.414L11.75 3.75a6 6 0 1 1 8.49 8.49z"/><path d="M16 8 2 22"/><path d="M17.488 15H9"/></svg>'
|
|
640
745
|
}
|
|
641
746
|
}));
|
|
642
747
|
ctx.subscriptions.push(ctx.ui.onDidOpenPanel((panel) => {
|
|
@@ -653,13 +758,14 @@ function activate(ctx) {
|
|
|
653
758
|
ctx.subscriptions.push(panel.onDidDispose(() => {
|
|
654
759
|
stopWatching(panel.id);
|
|
655
760
|
livePanelDocuments.delete(panel.id);
|
|
761
|
+
flushPendingDraftWrite(ctx, panel.id);
|
|
656
762
|
if (lastPanel === panel) lastPanel = void 0;
|
|
657
763
|
}));
|
|
658
764
|
void sendReady(ctx, panel).catch((error) => ctx.logger.warn(String(error)));
|
|
659
765
|
}));
|
|
660
766
|
ctx.subscriptions.push(ctx.tools.register({
|
|
661
767
|
name: "markdown_editor_document",
|
|
662
|
-
title: "
|
|
768
|
+
title: "\u5199\u4F5C",
|
|
663
769
|
description: `Open, create, revise, or restyle a Markdown document in Markdown Editor.
|
|
664
770
|
action:
|
|
665
771
|
open \u2014 read an absolute local Markdown path and open it as an editable WeChat article preview
|
|
@@ -686,12 +792,12 @@ action:
|
|
|
686
792
|
if (!path.isAbsolute(sourcePath)) return result("`path` must be an absolute local path.", true);
|
|
687
793
|
if (action === "open") {
|
|
688
794
|
try {
|
|
689
|
-
const markdown2 = await
|
|
795
|
+
const { markdown: markdown2, diskMarkdown, draftRestored, draftConflict } = await readFileWithDraft(ctx, sourcePath);
|
|
690
796
|
const panel = ctx.ui.createPanel({ instanceMode: "single", payload: { path: sourcePath } });
|
|
691
797
|
await panel.reveal();
|
|
692
798
|
watchSource(ctx, panel, sourcePath);
|
|
693
799
|
await rememberLastPath(ctx, panel, sourcePath);
|
|
694
|
-
await sendDocument(panel, { path: sourcePath, markdown: markdown2, title: documentTitle(markdown2, sourcePath) });
|
|
800
|
+
await sendDocument(panel, { path: sourcePath, markdown: markdown2, title: documentTitle(markdown2, sourcePath), draftRestored, draftConflict, diskMarkdown });
|
|
695
801
|
return result(`Opened Markdown Editor for ${path.basename(sourcePath)}.`);
|
|
696
802
|
} catch (error) {
|
|
697
803
|
return result(`Could not read ${sourcePath}: ${error instanceof Error ? error.message : String(error)}`, true);
|