@tnotesjs/core 0.6.1 → 0.8.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.
- package/commands/BaseCommand.ts +58 -0
- package/commands/build/BuildCommand.ts +25 -0
- package/commands/build/PreviewCommand.ts +29 -0
- package/commands/build/index.ts +8 -0
- package/commands/dev/DevCommand.ts +75 -0
- package/commands/dev/index.ts +7 -0
- package/commands/git/PullCommand.ts +25 -0
- package/commands/git/PushCommand.ts +64 -0
- package/commands/git/index.ts +8 -0
- package/commands/index.ts +11 -0
- package/commands/init-sub-repo/InitSubRepoCommand.ts +206 -0
- package/commands/init-sub-repo/index.ts +1 -0
- package/commands/misc/HelpCommand.ts +104 -0
- package/commands/misc/index.ts +7 -0
- package/commands/models.ts +87 -0
- package/commands/note/CreateNoteCommand.ts +160 -0
- package/commands/note/RenameNoteCommand.ts +147 -0
- package/commands/note/UpdateNoteConfigCommand.ts +78 -0
- package/commands/note/index.ts +9 -0
- package/commands/registry.ts +47 -0
- package/commands/update/UpdateCommand.ts +219 -0
- package/commands/update/index.ts +7 -0
- package/commands/update-completed-count/UpdateCompletedCountCommand.ts +208 -0
- package/commands/update-completed-count/index.ts +5 -0
- package/config/templates.ts +0 -5
- package/dist/{chunk-5F5RJ2HV.cjs → chunk-7FEANCT6.cjs} +16 -7
- package/dist/chunk-AGVER5MX.cjs +1746 -0
- package/dist/{chunk-TJ4527KA.cjs → chunk-BL22KD4V.cjs} +324 -950
- package/dist/{chunk-56PKXCOX.js → chunk-GWG75A5M.js} +13 -4
- package/dist/{chunk-DUTS75WQ.js → chunk-XNO3PCEH.js} +374 -1000
- package/dist/chunk-ZEQS64PE.js +1746 -0
- package/dist/cli/index.cjs +76 -155
- package/dist/cli/index.js +10 -89
- package/dist/markdown/index.cjs +8 -12
- package/dist/markdown/index.d.cts +1 -1
- package/dist/markdown/index.d.ts +1 -1
- package/dist/markdown/index.js +7 -11
- package/dist/{types-CwBWwNVv.d.ts → types-C_d0fFeD.d.ts} +8 -1
- package/dist/{types-CpsEy8lA.d.cts → types-MRBGgJTX.d.cts} +8 -1
- package/dist/vitepress/config/index.cjs +402 -178
- package/dist/vitepress/config/index.js +392 -168
- package/dist/workspace/index.cjs +4 -1202
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.ts +2 -2
- package/dist/workspace/index.js +4 -1202
- package/markdown/components.ts +86 -0
- package/markdown/index.ts +17 -0
- package/markdown/noteFormatter.test.ts +44 -0
- package/markdown/noteFormatter.ts +237 -0
- package/package.json +7 -3
- package/services/file-watcher/folderChangeHandler.ts +19 -15
- package/services/file-watcher/globalUpdateCoordinator.ts +21 -27
- package/services/file-watcher/service.ts +1 -6
- package/services/index.ts +0 -1
- package/services/note/service.ts +4 -6
- package/services/readme/service.ts +11 -18
- package/services/reconcileToc.ts +20 -0
- package/utils/index.ts +0 -9
- package/{services/toc → utils}/moveTocInside.test.ts +1 -1
- package/vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue +9 -18
- package/vitepress/components/EnWordList/EnWordList.vue +16 -662
- package/vitepress/components/Footprints/Footprints.vue +15 -537
- package/vitepress/components/Mermaid/Mermaid.vue +13 -588
- package/vitepress/components/MindmapPreview/MindmapPreview.vue +12 -434
- package/vitepress/components/MindmapPreview/markdown.ts +1 -1
- package/vitepress/components/NotesTable/NotesTable.vue +11 -130
- package/vitepress/components/constants.ts +2 -2
- package/vitepress/components/sidebar.data.ts +8 -5
- package/vitepress/configs/markdown.config.ts +170 -26
- package/vitepress/plugins/sidebarStructurePlugin.ts +242 -145
- package/vitepress/theme/index.ts +9 -13
- package/vitepress/theme/styles/base.scss +15 -0
- package/workspace/atomic.ts +113 -0
- package/workspace/errors.ts +27 -0
- package/workspace/index.ts +40 -0
- package/workspace/mutationQueue.ts +28 -0
- package/workspace/paths.ts +64 -0
- package/workspace/reconcile.test.ts +300 -0
- package/workspace/reconcile.ts +95 -0
- package/workspace/scanner.ts +292 -0
- package/workspace/types.ts +224 -0
- package/workspace/workspace.test.ts +333 -0
- package/workspace/workspace.ts +1020 -0
- package/dist/chunk-3R7QSABB.cjs +0 -502
- package/dist/chunk-CZXRQPFJ.js +0 -11
- package/dist/chunk-HXED7KVT.cjs +0 -11
- package/dist/chunk-U6IBLREL.js +0 -502
- package/services/toc/index.ts +0 -5
- package/services/toc/service.ts +0 -759
- package/utils/migrateReadmeToToc.test.ts +0 -111
- package/utils/migrateReadmeToToc.ts +0 -135
- package/vitepress/components/EnWordList/RightClickMenu.vue +0 -93
|
@@ -0,0 +1,1746 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatTNotesNote
|
|
3
|
+
} from "./chunk-GWG75A5M.js";
|
|
4
|
+
|
|
5
|
+
// workspace/workspace.ts
|
|
6
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
7
|
+
import fs3 from "fs/promises";
|
|
8
|
+
import path4 from "path";
|
|
9
|
+
|
|
10
|
+
// workspace/atomic.ts
|
|
11
|
+
import { randomUUID } from "crypto";
|
|
12
|
+
import { constants as fsConstants } from "fs";
|
|
13
|
+
import fs from "fs/promises";
|
|
14
|
+
import path from "path";
|
|
15
|
+
|
|
16
|
+
// workspace/errors.ts
|
|
17
|
+
var WorkspaceError = class extends Error {
|
|
18
|
+
constructor(code, message, details) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "WorkspaceError";
|
|
21
|
+
this.code = code;
|
|
22
|
+
this.details = details;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// workspace/atomic.ts
|
|
27
|
+
async function pathExists(filePath) {
|
|
28
|
+
try {
|
|
29
|
+
await fs.access(filePath, fsConstants.F_OK);
|
|
30
|
+
return true;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function stageWrite(write) {
|
|
36
|
+
await fs.mkdir(path.dirname(write.path), { recursive: true });
|
|
37
|
+
const temporaryPath = path.join(
|
|
38
|
+
path.dirname(write.path),
|
|
39
|
+
`.${path.basename(write.path)}.${randomUUID()}.tmp`
|
|
40
|
+
);
|
|
41
|
+
const handle = await fs.open(temporaryPath, "wx");
|
|
42
|
+
try {
|
|
43
|
+
await handle.writeFile(write.data);
|
|
44
|
+
await handle.sync();
|
|
45
|
+
} finally {
|
|
46
|
+
await handle.close();
|
|
47
|
+
}
|
|
48
|
+
return temporaryPath;
|
|
49
|
+
}
|
|
50
|
+
async function restoreOriginals(originals) {
|
|
51
|
+
for (const original of originals) {
|
|
52
|
+
if (original.existed && original.data) {
|
|
53
|
+
const restorePath = await stageWrite({
|
|
54
|
+
path: original.path,
|
|
55
|
+
data: original.data
|
|
56
|
+
});
|
|
57
|
+
await fs.rename(restorePath, original.path);
|
|
58
|
+
} else {
|
|
59
|
+
await fs.rm(original.path, { force: true });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function writeFilesAtomically(writes) {
|
|
64
|
+
const uniqueWrites = /* @__PURE__ */ new Map();
|
|
65
|
+
for (const write of writes) uniqueWrites.set(write.path, write);
|
|
66
|
+
const normalizedWrites = [...uniqueWrites.values()];
|
|
67
|
+
const originals = [];
|
|
68
|
+
const staged = [];
|
|
69
|
+
try {
|
|
70
|
+
for (const write of normalizedWrites) {
|
|
71
|
+
const existed = await pathExists(write.path);
|
|
72
|
+
originals.push({
|
|
73
|
+
path: write.path,
|
|
74
|
+
existed,
|
|
75
|
+
data: existed ? await fs.readFile(write.path) : void 0
|
|
76
|
+
});
|
|
77
|
+
staged.push({
|
|
78
|
+
target: write.path,
|
|
79
|
+
temporary: await stageWrite(write)
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
for (const item of staged) {
|
|
83
|
+
await fs.rename(item.temporary, item.target);
|
|
84
|
+
}
|
|
85
|
+
} catch (error) {
|
|
86
|
+
await Promise.allSettled(
|
|
87
|
+
staged.map((item) => fs.rm(item.temporary, { force: true }))
|
|
88
|
+
);
|
|
89
|
+
try {
|
|
90
|
+
await restoreOriginals(originals);
|
|
91
|
+
} catch (restoreError) {
|
|
92
|
+
throw new WorkspaceError(
|
|
93
|
+
"FILESYSTEM_ERROR",
|
|
94
|
+
"\u5199\u5165\u5931\u8D25\uFF0C\u5E76\u4E14\u65E0\u6CD5\u5B8C\u6574\u6062\u590D\u539F\u6587\u4EF6",
|
|
95
|
+
{
|
|
96
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
97
|
+
restoreCause: restoreError instanceof Error ? restoreError.message : String(restoreError)
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
throw new WorkspaceError("FILESYSTEM_ERROR", "\u65E0\u6CD5\u539F\u5B50\u5199\u5165\u77E5\u8BC6\u5E93\u6587\u4EF6", {
|
|
102
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// workspace/mutationQueue.ts
|
|
108
|
+
var MutationQueue = class {
|
|
109
|
+
constructor() {
|
|
110
|
+
this.tail = Promise.resolve();
|
|
111
|
+
this.disposed = false;
|
|
112
|
+
}
|
|
113
|
+
async run(operation) {
|
|
114
|
+
if (this.disposed) {
|
|
115
|
+
throw new Error("Mutation queue has been disposed");
|
|
116
|
+
}
|
|
117
|
+
const previous = this.tail;
|
|
118
|
+
let release;
|
|
119
|
+
this.tail = new Promise((resolve) => {
|
|
120
|
+
release = resolve;
|
|
121
|
+
});
|
|
122
|
+
await previous;
|
|
123
|
+
try {
|
|
124
|
+
return await operation();
|
|
125
|
+
} finally {
|
|
126
|
+
release();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async dispose() {
|
|
130
|
+
this.disposed = true;
|
|
131
|
+
await this.tail;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// workspace/paths.ts
|
|
136
|
+
import path2 from "path";
|
|
137
|
+
function createWorkspacePaths(rootPath) {
|
|
138
|
+
if (!rootPath || !path2.isAbsolute(rootPath)) {
|
|
139
|
+
throw new WorkspaceError(
|
|
140
|
+
"INVALID_PATH",
|
|
141
|
+
"\u77E5\u8BC6\u5E93\u8DEF\u5F84\u5FC5\u987B\u662F\u975E\u7A7A\u7684\u7EDD\u5BF9\u8DEF\u5F84",
|
|
142
|
+
{ rootPath }
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
const root = path2.normalize(path2.resolve(rootPath));
|
|
146
|
+
return {
|
|
147
|
+
root,
|
|
148
|
+
config: path2.join(root, ".tnotes.json"),
|
|
149
|
+
toc: path2.join(root, "TOC.md"),
|
|
150
|
+
notes: path2.join(root, "notes"),
|
|
151
|
+
sidebar: path2.join(root, "sidebar.json"),
|
|
152
|
+
packageJson: path2.join(root, "package.json")
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function assertPathInside(parent, target) {
|
|
156
|
+
const relative = path2.relative(parent, target);
|
|
157
|
+
if (relative === "" || !relative.startsWith(`..${path2.sep}`) && relative !== ".." && !path2.isAbsolute(relative)) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
throw new WorkspaceError("INVALID_PATH", "\u76EE\u6807\u8DEF\u5F84\u8D85\u51FA\u5141\u8BB8\u8303\u56F4", {
|
|
161
|
+
parent,
|
|
162
|
+
target
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function sanitizeFileName(fileName) {
|
|
166
|
+
const value = fileName.trim();
|
|
167
|
+
if (!value || value === "." || value === ".." || value.includes("/") || value.includes("\\") || value.includes("\0")) {
|
|
168
|
+
throw new WorkspaceError("INVALID_PATH", "\u9644\u4EF6\u6587\u4EF6\u540D\u4E0D\u5408\u6CD5", {
|
|
169
|
+
fileName
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return value;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// workspace/reconcile.ts
|
|
176
|
+
var NOTE_CONFIG_DIAGNOSTIC_CODES = /* @__PURE__ */ new Set([
|
|
177
|
+
"NOTE_CONFIG_MISSING",
|
|
178
|
+
"NOTE_CONFIG_INVALID"
|
|
179
|
+
]);
|
|
180
|
+
function dirFromConfigDiagnostic(path5) {
|
|
181
|
+
if (!path5) return null;
|
|
182
|
+
const match = path5.replace(/\\/g, "/").match(/(?:^|\/)notes\/([^/]+)\/\.tnotes\.json$/);
|
|
183
|
+
return match ? match[1] : null;
|
|
184
|
+
}
|
|
185
|
+
function planReconcile(snapshot) {
|
|
186
|
+
const seen = /* @__PURE__ */ new Set();
|
|
187
|
+
const trashDirs = [];
|
|
188
|
+
for (const diagnostic of snapshot.health.diagnostics) {
|
|
189
|
+
if (!NOTE_CONFIG_DIAGNOSTIC_CODES.has(diagnostic.code)) continue;
|
|
190
|
+
const dir = dirFromConfigDiagnostic(diagnostic.path);
|
|
191
|
+
if (dir && !seen.has(dir)) {
|
|
192
|
+
seen.add(dir);
|
|
193
|
+
trashDirs.push(dir);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
trashDirs.sort((a, b) => a.localeCompare(b));
|
|
197
|
+
const validIndexes = new Set(snapshot.notes.map((note) => note.index));
|
|
198
|
+
const clean = (nodes) => {
|
|
199
|
+
const next = [];
|
|
200
|
+
for (const node of nodes) {
|
|
201
|
+
if (node.kind === "folder") {
|
|
202
|
+
next.push({ ...node, children: clean(node.children) });
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (validIndexes.has(node.noteIndex)) {
|
|
206
|
+
next.push({ ...node, children: clean(node.children) });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return next;
|
|
210
|
+
};
|
|
211
|
+
const present = /* @__PURE__ */ new Set();
|
|
212
|
+
const walk = (nodes) => {
|
|
213
|
+
for (const node of nodes) {
|
|
214
|
+
if (node.kind === "note") present.add(node.noteIndex);
|
|
215
|
+
walk(node.children);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
walk(snapshot.toc);
|
|
219
|
+
const missing = snapshot.notes.filter((note) => !present.has(note.index)).sort((left, right) => left.index.localeCompare(right.index));
|
|
220
|
+
const tree = [
|
|
221
|
+
...clean(snapshot.toc),
|
|
222
|
+
...missing.map((note) => ({
|
|
223
|
+
kind: "note",
|
|
224
|
+
noteIndex: note.index,
|
|
225
|
+
indent: 0,
|
|
226
|
+
tocLineIndex: 0,
|
|
227
|
+
children: []
|
|
228
|
+
}))
|
|
229
|
+
];
|
|
230
|
+
return { trashDirs, tree };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// workspace/scanner.ts
|
|
234
|
+
import { createHash } from "crypto";
|
|
235
|
+
import fs2 from "fs/promises";
|
|
236
|
+
import path3 from "path";
|
|
237
|
+
|
|
238
|
+
// utils/tocNodeId.ts
|
|
239
|
+
function extractNoteIndexFromLink(link) {
|
|
240
|
+
if (!link) return null;
|
|
241
|
+
const match = link.match(/\/notes\/(\d{4})\./);
|
|
242
|
+
return match ? match[1] : null;
|
|
243
|
+
}
|
|
244
|
+
function nodeIdForNote(noteIndex) {
|
|
245
|
+
return `note:${noteIndex}`;
|
|
246
|
+
}
|
|
247
|
+
function nodeIdForFolder(folderPath) {
|
|
248
|
+
return `folder:${folderPath.join("/")}`;
|
|
249
|
+
}
|
|
250
|
+
function nodeIdForLine(tocLineIndex) {
|
|
251
|
+
return `line:${tocLineIndex}`;
|
|
252
|
+
}
|
|
253
|
+
function computeSidebarNodeId(item) {
|
|
254
|
+
if (item.nodeId) return item.nodeId;
|
|
255
|
+
const noteIndex = extractNoteIndexFromLink(item.link);
|
|
256
|
+
if (noteIndex) return nodeIdForNote(noteIndex);
|
|
257
|
+
if (item.tocLineIndex !== void 0 && !item.link) {
|
|
258
|
+
return nodeIdForLine(item.tocLineIndex);
|
|
259
|
+
}
|
|
260
|
+
if (item.folderPath?.length) {
|
|
261
|
+
return nodeIdForFolder(item.folderPath);
|
|
262
|
+
}
|
|
263
|
+
if (item.tocLineIndex !== void 0) {
|
|
264
|
+
return nodeIdForLine(item.tocLineIndex);
|
|
265
|
+
}
|
|
266
|
+
return `text:${item.text}`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// utils/tocHelpers.ts
|
|
270
|
+
var TOC_INDENT_SPACES = 2;
|
|
271
|
+
var TOC_LEGACY_FULL_REGEX = /^( *)(-\s+\[(x| )\])\s+\[(\d{4}\.[^\]]+)\]\(([^)]+)\)/;
|
|
272
|
+
var TOC_NOTE_LINE_REGEX = /^( *)(-\s+\[(x| )\])\s+(\d{4})(?:\.\s*(.*))?\s*$/;
|
|
273
|
+
var TOC_FOLDER_LINE_REGEX = /^( *)(-\s+(?!\[(?:x| )\]).+?)\s*$/;
|
|
274
|
+
function extractNoteIndexFromTitle(text) {
|
|
275
|
+
const match = text.match(/^(\d{4})\./);
|
|
276
|
+
return match ? match[1] : null;
|
|
277
|
+
}
|
|
278
|
+
function parseIndent(spaces) {
|
|
279
|
+
return Math.floor((spaces?.length ?? 0) / TOC_INDENT_SPACES);
|
|
280
|
+
}
|
|
281
|
+
function parseTocLine(line) {
|
|
282
|
+
const rawLine = line ?? "";
|
|
283
|
+
const empty = {
|
|
284
|
+
kind: "unknown",
|
|
285
|
+
isMatch: false,
|
|
286
|
+
indentLevel: 0,
|
|
287
|
+
noteIndex: null,
|
|
288
|
+
folderTitle: null,
|
|
289
|
+
completed: false,
|
|
290
|
+
rawLine
|
|
291
|
+
};
|
|
292
|
+
if (line == null) return empty;
|
|
293
|
+
const legacyMatch = line.match(TOC_LEGACY_FULL_REGEX);
|
|
294
|
+
if (legacyMatch) {
|
|
295
|
+
const [, spaces, , statusChar, titleText] = legacyMatch;
|
|
296
|
+
const noteIndex = extractNoteIndexFromTitle(titleText);
|
|
297
|
+
if (!noteIndex) return empty;
|
|
298
|
+
return {
|
|
299
|
+
kind: "note",
|
|
300
|
+
isMatch: true,
|
|
301
|
+
indentLevel: parseIndent(spaces),
|
|
302
|
+
noteIndex,
|
|
303
|
+
folderTitle: null,
|
|
304
|
+
completed: statusChar === "x",
|
|
305
|
+
rawLine
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
const noteMatch = line.match(TOC_NOTE_LINE_REGEX);
|
|
309
|
+
if (noteMatch) {
|
|
310
|
+
const [, spaces, , statusChar, noteIndex] = noteMatch;
|
|
311
|
+
return {
|
|
312
|
+
kind: "note",
|
|
313
|
+
isMatch: true,
|
|
314
|
+
indentLevel: parseIndent(spaces),
|
|
315
|
+
noteIndex,
|
|
316
|
+
folderTitle: null,
|
|
317
|
+
completed: statusChar === "x",
|
|
318
|
+
rawLine
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
const folderMatch = line.match(TOC_FOLDER_LINE_REGEX);
|
|
322
|
+
if (folderMatch) {
|
|
323
|
+
const [, spaces, titlePart] = folderMatch;
|
|
324
|
+
const title = titlePart.replace(/^-\s+/, "").trim();
|
|
325
|
+
if (!title) return empty;
|
|
326
|
+
return {
|
|
327
|
+
kind: "folder",
|
|
328
|
+
isMatch: true,
|
|
329
|
+
indentLevel: parseIndent(spaces),
|
|
330
|
+
noteIndex: null,
|
|
331
|
+
folderTitle: title,
|
|
332
|
+
completed: false,
|
|
333
|
+
rawLine
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
return empty;
|
|
337
|
+
}
|
|
338
|
+
function isTocContentLine(line) {
|
|
339
|
+
return parseTocLine(line).isMatch;
|
|
340
|
+
}
|
|
341
|
+
function resolveNoteFromIndex(index, notes) {
|
|
342
|
+
return notes.find((n) => n.index === index);
|
|
343
|
+
}
|
|
344
|
+
function getTocLineCompleted(note, configOverride) {
|
|
345
|
+
const config = configOverride ? { ...note.config, ...configOverride } : note.config;
|
|
346
|
+
return config?.done ?? false;
|
|
347
|
+
}
|
|
348
|
+
function buildFolderTocLine(title, indentLevel) {
|
|
349
|
+
const indent = " ".repeat(indentLevel * TOC_INDENT_SPACES);
|
|
350
|
+
return `${indent}- ${title}`;
|
|
351
|
+
}
|
|
352
|
+
function buildTocLine(note, indentLevel, completed) {
|
|
353
|
+
const indent = " ".repeat(indentLevel * TOC_INDENT_SPACES);
|
|
354
|
+
const status = completed ?? getTocLineCompleted(note) ? "x" : " ";
|
|
355
|
+
return `${indent}- [${status}] ${note.dirName}`;
|
|
356
|
+
}
|
|
357
|
+
function mutableToTreeNode(node) {
|
|
358
|
+
if (node.kind === "folder") {
|
|
359
|
+
return {
|
|
360
|
+
kind: "folder",
|
|
361
|
+
title: node.title,
|
|
362
|
+
indent: node.indent,
|
|
363
|
+
tocLineIndex: node.tocLineIndex,
|
|
364
|
+
children: node.children.map(mutableToTreeNode)
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
kind: "note",
|
|
369
|
+
noteIndex: node.noteIndex,
|
|
370
|
+
indent: node.indent,
|
|
371
|
+
tocLineIndex: node.tocLineIndex,
|
|
372
|
+
children: node.children.map(mutableToTreeNode)
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
function buildMutableTreeFromFlat(flatNodes) {
|
|
376
|
+
const roots = [];
|
|
377
|
+
const stack = [];
|
|
378
|
+
for (const item of flatNodes) {
|
|
379
|
+
while (stack.length > 0 && stack[stack.length - 1].indent >= item.indent) {
|
|
380
|
+
stack.pop();
|
|
381
|
+
}
|
|
382
|
+
const node = {
|
|
383
|
+
kind: item.kind,
|
|
384
|
+
title: item.title,
|
|
385
|
+
noteIndex: item.noteIndex,
|
|
386
|
+
indent: item.indent,
|
|
387
|
+
tocLineIndex: item.tocLineIndex,
|
|
388
|
+
children: []
|
|
389
|
+
};
|
|
390
|
+
if (stack.length === 0) {
|
|
391
|
+
roots.push(node);
|
|
392
|
+
} else {
|
|
393
|
+
stack[stack.length - 1].children.push(node);
|
|
394
|
+
}
|
|
395
|
+
stack.push(node);
|
|
396
|
+
}
|
|
397
|
+
return roots;
|
|
398
|
+
}
|
|
399
|
+
function parseTocToMutableTree(lines, notes) {
|
|
400
|
+
const flatNodes = [];
|
|
401
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
402
|
+
const line = lines[lineIndex];
|
|
403
|
+
const parsed = parseTocLine(line);
|
|
404
|
+
if (!parsed.isMatch) continue;
|
|
405
|
+
if (parsed.kind === "folder") {
|
|
406
|
+
flatNodes.push({
|
|
407
|
+
kind: "folder",
|
|
408
|
+
title: parsed.folderTitle,
|
|
409
|
+
indent: parsed.indentLevel,
|
|
410
|
+
tocLineIndex: lineIndex
|
|
411
|
+
});
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (!parsed.noteIndex) continue;
|
|
415
|
+
if (!resolveNoteFromIndex(parsed.noteIndex, notes)) continue;
|
|
416
|
+
flatNodes.push({
|
|
417
|
+
kind: "note",
|
|
418
|
+
noteIndex: parsed.noteIndex,
|
|
419
|
+
indent: parsed.indentLevel,
|
|
420
|
+
tocLineIndex: lineIndex
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
return buildMutableTreeFromFlat(flatNodes);
|
|
424
|
+
}
|
|
425
|
+
function parseTocToTree(lines, notes) {
|
|
426
|
+
return parseTocToMutableTree(lines, notes).map(mutableToTreeNode);
|
|
427
|
+
}
|
|
428
|
+
function serializeTocTree(tree, notes, configByIndex) {
|
|
429
|
+
const lines = [];
|
|
430
|
+
function walk(nodes) {
|
|
431
|
+
for (const node of nodes) {
|
|
432
|
+
if (node.kind === "folder") {
|
|
433
|
+
lines.push(buildFolderTocLine(node.title, node.indent));
|
|
434
|
+
walk(node.children);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
const note = resolveNoteFromIndex(node.noteIndex, notes);
|
|
438
|
+
if (!note) continue;
|
|
439
|
+
const override = configByIndex?.get(node.noteIndex);
|
|
440
|
+
const completed = override !== void 0 ? getTocLineCompleted(note, override) : getTocLineCompleted(note);
|
|
441
|
+
lines.push(buildTocLine(note, node.indent, completed));
|
|
442
|
+
walk(node.children);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
walk(tree);
|
|
446
|
+
return lines;
|
|
447
|
+
}
|
|
448
|
+
function adjustTocLineIndexAfterSubtreeRemoval(lineIndex, removedStart, removedEnd) {
|
|
449
|
+
if (lineIndex >= removedEnd) {
|
|
450
|
+
return lineIndex - (removedEnd - removedStart);
|
|
451
|
+
}
|
|
452
|
+
return lineIndex;
|
|
453
|
+
}
|
|
454
|
+
function getTocEntrySubtreeRange(lines, lineIndex) {
|
|
455
|
+
const parsed = parseTocLine(lines[lineIndex]);
|
|
456
|
+
if (!parsed.isMatch) {
|
|
457
|
+
return { start: lineIndex, end: lineIndex + 1 };
|
|
458
|
+
}
|
|
459
|
+
const baseIndent = parsed.indentLevel;
|
|
460
|
+
let end = lineIndex + 1;
|
|
461
|
+
for (let i = lineIndex + 1; i < lines.length; i++) {
|
|
462
|
+
const next = parseTocLine(lines[i]);
|
|
463
|
+
if (next.isMatch && next.indentLevel <= baseIndent) break;
|
|
464
|
+
end = i + 1;
|
|
465
|
+
}
|
|
466
|
+
return { start: lineIndex, end };
|
|
467
|
+
}
|
|
468
|
+
function collectNoteIndexesInSubtree(lines, lineIndex) {
|
|
469
|
+
const { start, end } = getTocEntrySubtreeRange(lines, lineIndex);
|
|
470
|
+
const indexes = [];
|
|
471
|
+
for (let i = start; i < end; i++) {
|
|
472
|
+
const parsed = parseTocLine(lines[i]);
|
|
473
|
+
if (parsed.noteIndex) {
|
|
474
|
+
indexes.push(parsed.noteIndex);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return indexes;
|
|
478
|
+
}
|
|
479
|
+
function renameFolderLine(lines, lineIndex, newTitle) {
|
|
480
|
+
const parsed = parseTocLine(lines[lineIndex]);
|
|
481
|
+
if (parsed.kind !== "folder") {
|
|
482
|
+
throw new Error(`TOC \u884C ${lineIndex} \u4E0D\u662F\u76EE\u5F55\u884C`);
|
|
483
|
+
}
|
|
484
|
+
const trimmed = newTitle.trim();
|
|
485
|
+
if (!trimmed) {
|
|
486
|
+
throw new Error("\u76EE\u5F55\u6807\u9898\u4E0D\u80FD\u4E3A\u7A7A");
|
|
487
|
+
}
|
|
488
|
+
const result = [...lines];
|
|
489
|
+
result[lineIndex] = buildFolderTocLine(trimmed, parsed.indentLevel);
|
|
490
|
+
return result;
|
|
491
|
+
}
|
|
492
|
+
function findFolderLineIndex(lines, folderPath) {
|
|
493
|
+
const target = folderPath.join("/");
|
|
494
|
+
const stack = [];
|
|
495
|
+
for (let i = 0; i < lines.length; i++) {
|
|
496
|
+
const parsed = parseTocLine(lines[i]);
|
|
497
|
+
if (!parsed.isMatch) continue;
|
|
498
|
+
while (stack.length > 0 && stack[stack.length - 1].indent >= parsed.indentLevel) {
|
|
499
|
+
stack.pop();
|
|
500
|
+
}
|
|
501
|
+
if (parsed.kind === "folder") {
|
|
502
|
+
const path5 = [...stack.map((s) => s.title), parsed.folderTitle].join("/");
|
|
503
|
+
if (path5 === target) return i;
|
|
504
|
+
stack.push({ title: parsed.folderTitle, indent: parsed.indentLevel });
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
throw new Error(`TOC.md \u4E2D\u672A\u627E\u5230\u76EE\u5F55: ${folderPath.join(" > ")}`);
|
|
508
|
+
}
|
|
509
|
+
function findTocLineIndex(lines, noteIndex) {
|
|
510
|
+
for (let i = 0; i < lines.length; i++) {
|
|
511
|
+
const parsed = parseTocLine(lines[i]);
|
|
512
|
+
if (parsed.noteIndex === noteIndex) return i;
|
|
513
|
+
}
|
|
514
|
+
throw new Error(`TOC.md \u4E2D\u672A\u627E\u5230\u7B14\u8BB0: ${noteIndex}`);
|
|
515
|
+
}
|
|
516
|
+
function processTocEmptyLines(lines) {
|
|
517
|
+
const result = [];
|
|
518
|
+
let previousEmpty = false;
|
|
519
|
+
for (let i = 0; i < lines.length; i++) {
|
|
520
|
+
const line = lines[i];
|
|
521
|
+
if (line === "") {
|
|
522
|
+
const prev = i > 0 ? lines[i - 1] : null;
|
|
523
|
+
const next = i < lines.length - 1 ? lines[i + 1] : null;
|
|
524
|
+
if (prev && next && isTocContentLine(prev) && isTocContentLine(next)) {
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
if (!previousEmpty) {
|
|
528
|
+
result.push(line);
|
|
529
|
+
previousEmpty = true;
|
|
530
|
+
}
|
|
531
|
+
} else {
|
|
532
|
+
result.push(line);
|
|
533
|
+
previousEmpty = false;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return result;
|
|
537
|
+
}
|
|
538
|
+
function parseTocCompletedNotes(content) {
|
|
539
|
+
const lines = content.split("\n");
|
|
540
|
+
const noteMap = /* @__PURE__ */ new Map();
|
|
541
|
+
for (const line of lines) {
|
|
542
|
+
const parsed = parseTocLine(line);
|
|
543
|
+
if (parsed.kind !== "note" || !parsed.noteIndex) continue;
|
|
544
|
+
const noteIndex = parsed.noteIndex;
|
|
545
|
+
const completed = parsed.completed;
|
|
546
|
+
if (noteMap.has(noteIndex)) {
|
|
547
|
+
const existing = noteMap.get(noteIndex);
|
|
548
|
+
if (existing.completed !== completed) {
|
|
549
|
+
throw new Error(
|
|
550
|
+
`\u53D1\u73B0\u76F8\u540C\u7F16\u53F7 ${noteIndex} \u7684\u7B14\u8BB0\u6709\u4E0D\u540C\u7684\u5B8C\u6210\u72B6\u6001:
|
|
551
|
+
\u7B2C\u4E00\u6B21\u51FA\u73B0: ${existing.line}
|
|
552
|
+
\u7B2C\u4E8C\u6B21\u51FA\u73B0: ${line.trim()}`
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
noteMap.set(noteIndex, {
|
|
558
|
+
noteIndex,
|
|
559
|
+
completed,
|
|
560
|
+
line: line.trim()
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
const notes = Array.from(noteMap.values());
|
|
564
|
+
return {
|
|
565
|
+
completedCount: notes.filter((n) => n.completed).length,
|
|
566
|
+
totalCount: notes.length,
|
|
567
|
+
notes
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
function buildSidebarFromTocTree(tree, notes, options, parentFolderPath = []) {
|
|
571
|
+
const collapsed = options.sidebarIsCollapsed ?? true;
|
|
572
|
+
function mapNote(node, currentFolderPath) {
|
|
573
|
+
const note = resolveNoteFromIndex(node.noteIndex, notes);
|
|
574
|
+
if (!note) return null;
|
|
575
|
+
let statusEmoji = "\u23F0 ";
|
|
576
|
+
if (note.config?.done) {
|
|
577
|
+
statusEmoji = "\u2705 ";
|
|
578
|
+
}
|
|
579
|
+
let displayText = note.dirName;
|
|
580
|
+
if (!options.sidebarShowNoteId) {
|
|
581
|
+
displayText = note.dirName.replace(/^\d{4}\.\s/, "");
|
|
582
|
+
}
|
|
583
|
+
const childItems = node.children.map((child) => mapNode(child, currentFolderPath)).filter((item2) => item2 !== null);
|
|
584
|
+
if (childItems.length > 0) {
|
|
585
|
+
const item2 = {
|
|
586
|
+
text: statusEmoji + displayText,
|
|
587
|
+
link: `/notes/${note.dirName}/README`,
|
|
588
|
+
collapsed,
|
|
589
|
+
items: childItems,
|
|
590
|
+
tocLineIndex: node.tocLineIndex
|
|
591
|
+
};
|
|
592
|
+
item2.nodeId = computeSidebarNodeId(item2);
|
|
593
|
+
return item2;
|
|
594
|
+
}
|
|
595
|
+
const item = {
|
|
596
|
+
text: statusEmoji + displayText,
|
|
597
|
+
link: `/notes/${note.dirName}/README`,
|
|
598
|
+
tocLineIndex: node.tocLineIndex
|
|
599
|
+
};
|
|
600
|
+
item.nodeId = computeSidebarNodeId(item);
|
|
601
|
+
return item;
|
|
602
|
+
}
|
|
603
|
+
function mapFolder(node, folderPath) {
|
|
604
|
+
const childItems = node.children.map((child) => mapNode(child, folderPath)).filter((item2) => item2 !== null);
|
|
605
|
+
if (childItems.length === 0) {
|
|
606
|
+
const item2 = {
|
|
607
|
+
text: node.title,
|
|
608
|
+
collapsed,
|
|
609
|
+
items: [],
|
|
610
|
+
folderPath,
|
|
611
|
+
tocLineIndex: node.tocLineIndex
|
|
612
|
+
};
|
|
613
|
+
item2.nodeId = computeSidebarNodeId(item2);
|
|
614
|
+
return item2;
|
|
615
|
+
}
|
|
616
|
+
const item = {
|
|
617
|
+
text: node.title,
|
|
618
|
+
collapsed,
|
|
619
|
+
items: childItems,
|
|
620
|
+
folderPath,
|
|
621
|
+
tocLineIndex: node.tocLineIndex
|
|
622
|
+
};
|
|
623
|
+
item.nodeId = computeSidebarNodeId(item);
|
|
624
|
+
return item;
|
|
625
|
+
}
|
|
626
|
+
function mapNode(node, currentFolderPath) {
|
|
627
|
+
if (node.kind === "folder") {
|
|
628
|
+
const folderPath = [...currentFolderPath, node.title];
|
|
629
|
+
return mapFolder(node, folderPath);
|
|
630
|
+
}
|
|
631
|
+
return mapNote(node, currentFolderPath);
|
|
632
|
+
}
|
|
633
|
+
return tree.map((node) => mapNode(node, parentFolderPath)).filter((item) => item !== null);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// workspace/scanner.ts
|
|
637
|
+
var NOTE_DIRECTORY_PATTERN = /^(\d{4})\.\s*(.+)$/;
|
|
638
|
+
var CURRENT_SCHEMA_VERSION = 1;
|
|
639
|
+
function digest(...values) {
|
|
640
|
+
const hash = createHash("sha256");
|
|
641
|
+
for (const value of values) {
|
|
642
|
+
hash.update(value);
|
|
643
|
+
hash.update("\0");
|
|
644
|
+
}
|
|
645
|
+
return hash.digest("hex");
|
|
646
|
+
}
|
|
647
|
+
async function readText(filePath) {
|
|
648
|
+
try {
|
|
649
|
+
return await fs2.readFile(filePath, "utf8");
|
|
650
|
+
} catch {
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
function parseJsonObject(content) {
|
|
655
|
+
try {
|
|
656
|
+
const value = JSON.parse(content);
|
|
657
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
658
|
+
} catch {
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
function noteInfoFromSummary(note) {
|
|
663
|
+
return {
|
|
664
|
+
index: note.index,
|
|
665
|
+
path: note.directoryPath,
|
|
666
|
+
dirName: note.dirName,
|
|
667
|
+
readmePath: note.readmePath,
|
|
668
|
+
configPath: note.configPath,
|
|
669
|
+
config: note.config
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
async function scanWorkspace(paths) {
|
|
673
|
+
const diagnostics = [];
|
|
674
|
+
let rootIsDirectory = false;
|
|
675
|
+
try {
|
|
676
|
+
rootIsDirectory = (await fs2.stat(paths.root)).isDirectory();
|
|
677
|
+
} catch {
|
|
678
|
+
}
|
|
679
|
+
if (!rootIsDirectory) {
|
|
680
|
+
diagnostics.push({
|
|
681
|
+
code: "ROOT_NOT_DIRECTORY",
|
|
682
|
+
message: "\u77E5\u8BC6\u5E93\u6839\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u662F\u76EE\u5F55",
|
|
683
|
+
severity: "error",
|
|
684
|
+
path: paths.root
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
const configText = await readText(paths.config);
|
|
688
|
+
let config = null;
|
|
689
|
+
if (configText === null) {
|
|
690
|
+
diagnostics.push({
|
|
691
|
+
code: "CONFIG_MISSING",
|
|
692
|
+
message: "\u7F3A\u5C11\u77E5\u8BC6\u5E93\u914D\u7F6E .tnotes.json",
|
|
693
|
+
severity: "error",
|
|
694
|
+
path: paths.config
|
|
695
|
+
});
|
|
696
|
+
} else {
|
|
697
|
+
config = parseJsonObject(configText);
|
|
698
|
+
if (!config) {
|
|
699
|
+
diagnostics.push({
|
|
700
|
+
code: "CONFIG_INVALID_JSON",
|
|
701
|
+
message: "\u77E5\u8BC6\u5E93\u914D\u7F6E\u4E0D\u662F\u6709\u6548\u7684 JSON \u5BF9\u8C61",
|
|
702
|
+
severity: "error",
|
|
703
|
+
path: paths.config
|
|
704
|
+
});
|
|
705
|
+
} else {
|
|
706
|
+
if (typeof config.id !== "string" || !config.id.trim()) {
|
|
707
|
+
diagnostics.push({
|
|
708
|
+
code: "CONFIG_ID_MISSING",
|
|
709
|
+
message: "\u77E5\u8BC6\u5E93\u914D\u7F6E\u7F3A\u5C11\u7A33\u5B9A\u7684 id",
|
|
710
|
+
severity: "error",
|
|
711
|
+
path: paths.config
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
if (typeof config.repoName !== "string" || !config.repoName.trim()) {
|
|
715
|
+
diagnostics.push({
|
|
716
|
+
code: "CONFIG_REPO_NAME_MISSING",
|
|
717
|
+
message: "\u77E5\u8BC6\u5E93\u914D\u7F6E\u7F3A\u5C11 repoName",
|
|
718
|
+
severity: "error",
|
|
719
|
+
path: paths.config
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
let notesDirectoryExists = false;
|
|
725
|
+
try {
|
|
726
|
+
notesDirectoryExists = (await fs2.stat(paths.notes)).isDirectory();
|
|
727
|
+
} catch {
|
|
728
|
+
}
|
|
729
|
+
if (!notesDirectoryExists) {
|
|
730
|
+
diagnostics.push({
|
|
731
|
+
code: "NOTES_DIRECTORY_MISSING",
|
|
732
|
+
message: "\u7F3A\u5C11 notes \u76EE\u5F55",
|
|
733
|
+
severity: "error",
|
|
734
|
+
path: paths.notes
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
const tocText = await readText(paths.toc);
|
|
738
|
+
if (tocText === null) {
|
|
739
|
+
diagnostics.push({
|
|
740
|
+
code: "TOC_MISSING",
|
|
741
|
+
message: "\u7F3A\u5C11\u76EE\u5F55\u771F\u76F8\u6E90 TOC.md",
|
|
742
|
+
severity: "error",
|
|
743
|
+
path: paths.toc
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
const notes = [];
|
|
747
|
+
const usedIndexes = /* @__PURE__ */ new Map();
|
|
748
|
+
const usedIds = /* @__PURE__ */ new Map();
|
|
749
|
+
if (notesDirectoryExists) {
|
|
750
|
+
const entries = await fs2.readdir(paths.notes, { withFileTypes: true });
|
|
751
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
752
|
+
for (const entry of entries) {
|
|
753
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
754
|
+
const match = NOTE_DIRECTORY_PATTERN.exec(entry.name);
|
|
755
|
+
if (!match) continue;
|
|
756
|
+
const [, index, rawTitle] = match;
|
|
757
|
+
const title = rawTitle.trim();
|
|
758
|
+
const directoryPath = path3.join(paths.notes, entry.name);
|
|
759
|
+
const readmePath = path3.join(directoryPath, "README.md");
|
|
760
|
+
const configPath = path3.join(directoryPath, ".tnotes.json");
|
|
761
|
+
const readme = await readText(readmePath);
|
|
762
|
+
const noteConfigText = await readText(configPath);
|
|
763
|
+
if (readme === null) {
|
|
764
|
+
diagnostics.push({
|
|
765
|
+
code: "NOTE_README_MISSING",
|
|
766
|
+
message: `\u7B14\u8BB0 ${entry.name} \u7F3A\u5C11 README.md`,
|
|
767
|
+
severity: "error",
|
|
768
|
+
path: readmePath
|
|
769
|
+
});
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
if (noteConfigText === null) {
|
|
773
|
+
diagnostics.push({
|
|
774
|
+
code: "NOTE_CONFIG_MISSING",
|
|
775
|
+
message: `\u7B14\u8BB0 ${entry.name} \u7F3A\u5C11 .tnotes.json`,
|
|
776
|
+
severity: "error",
|
|
777
|
+
path: configPath
|
|
778
|
+
});
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
const noteConfig = parseJsonObject(noteConfigText);
|
|
782
|
+
if (!noteConfig || typeof noteConfig.id !== "string" || !noteConfig.id) {
|
|
783
|
+
diagnostics.push({
|
|
784
|
+
code: "NOTE_CONFIG_INVALID",
|
|
785
|
+
message: `\u7B14\u8BB0 ${entry.name} \u7684\u914D\u7F6E\u65E0\u6CD5\u89E3\u6790\u6216\u7F3A\u5C11 id`,
|
|
786
|
+
severity: "error",
|
|
787
|
+
path: configPath
|
|
788
|
+
});
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
const existingIndex = usedIndexes.get(index);
|
|
792
|
+
if (existingIndex) {
|
|
793
|
+
diagnostics.push({
|
|
794
|
+
code: "NOTE_INDEX_DUPLICATE",
|
|
795
|
+
message: `\u7B14\u8BB0\u7F16\u53F7 ${index} \u91CD\u590D\uFF1A${existingIndex}\u3001${entry.name}`,
|
|
796
|
+
severity: "error",
|
|
797
|
+
path: directoryPath
|
|
798
|
+
});
|
|
799
|
+
} else {
|
|
800
|
+
usedIndexes.set(index, entry.name);
|
|
801
|
+
}
|
|
802
|
+
const existingId = usedIds.get(noteConfig.id);
|
|
803
|
+
if (existingId) {
|
|
804
|
+
diagnostics.push({
|
|
805
|
+
code: "NOTE_ID_DUPLICATE",
|
|
806
|
+
message: `\u7B14\u8BB0 id ${noteConfig.id} \u91CD\u590D\uFF1A${existingId}\u3001${entry.name}`,
|
|
807
|
+
severity: "error",
|
|
808
|
+
path: configPath
|
|
809
|
+
});
|
|
810
|
+
} else {
|
|
811
|
+
usedIds.set(noteConfig.id, entry.name);
|
|
812
|
+
}
|
|
813
|
+
notes.push({
|
|
814
|
+
uuid: noteConfig.id,
|
|
815
|
+
index,
|
|
816
|
+
title,
|
|
817
|
+
dirName: entry.name,
|
|
818
|
+
directoryPath,
|
|
819
|
+
readmePath,
|
|
820
|
+
configPath,
|
|
821
|
+
config: noteConfig,
|
|
822
|
+
revision: digest(entry.name, readme, noteConfigText)
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
const noteInfos = notes.map(noteInfoFromSummary);
|
|
827
|
+
const toc = tocText ? parseTocToTree(tocText.split("\n"), noteInfos) : [];
|
|
828
|
+
const sidebar = buildSidebarFromTocTree(toc, noteInfos, {
|
|
829
|
+
sidebarShowNoteId: config?.sidebarShowNoteId ?? true,
|
|
830
|
+
sidebarIsCollapsed: true
|
|
831
|
+
});
|
|
832
|
+
const schemaVersion = config?.schemaVersion;
|
|
833
|
+
const futureSchema = typeof schemaVersion === "number" && schemaVersion > CURRENT_SCHEMA_VERSION;
|
|
834
|
+
if (futureSchema) {
|
|
835
|
+
diagnostics.push({
|
|
836
|
+
code: "FUTURE_SCHEMA",
|
|
837
|
+
message: `\u77E5\u8BC6\u5E93 schemaVersion ${schemaVersion} \u9AD8\u4E8E\u5F53\u524D\u652F\u6301\u7248\u672C ${CURRENT_SCHEMA_VERSION}`,
|
|
838
|
+
severity: "error",
|
|
839
|
+
path: paths.config
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
const health = futureSchema ? { status: "future-schema", diagnostics } : diagnostics.some((diagnostic) => diagnostic.severity === "error") ? { status: "invalid", diagnostics } : { status: "ready", diagnostics };
|
|
843
|
+
const id = config && typeof config.id === "string" && config.id.trim() ? config.id : `path-${digest(paths.root).slice(0, 24)}`;
|
|
844
|
+
const revision = digest(
|
|
845
|
+
paths.root,
|
|
846
|
+
configText ?? "",
|
|
847
|
+
tocText ?? "",
|
|
848
|
+
...notes.map((note) => `${note.uuid}:${note.revision}`)
|
|
849
|
+
);
|
|
850
|
+
return {
|
|
851
|
+
snapshot: {
|
|
852
|
+
id,
|
|
853
|
+
rootPath: paths.root,
|
|
854
|
+
config,
|
|
855
|
+
health,
|
|
856
|
+
toc,
|
|
857
|
+
sidebar,
|
|
858
|
+
notes,
|
|
859
|
+
revision
|
|
860
|
+
},
|
|
861
|
+
configText,
|
|
862
|
+
tocText
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
function toNoteInfo(note) {
|
|
866
|
+
return noteInfoFromSummary(note);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// config/templates.ts
|
|
870
|
+
var NEW_NOTES_README_MD_TEMPLATE = `
|
|
871
|
+
<!-- region:toc -->
|
|
872
|
+
|
|
873
|
+
- [1. \u672C\u8282\u5185\u5BB9](#1-\u672C\u8282\u5185\u5BB9)
|
|
874
|
+
|
|
875
|
+
<!-- endregion:toc -->
|
|
876
|
+
|
|
877
|
+
## 1. \u672C\u8282\u5185\u5BB9
|
|
878
|
+
|
|
879
|
+
- todo
|
|
880
|
+
`;
|
|
881
|
+
function getNewNoteReadmeBody() {
|
|
882
|
+
return NEW_NOTES_README_MD_TEMPLATE;
|
|
883
|
+
}
|
|
884
|
+
function generateNoteTitle(noteIndex, title, repoUrl) {
|
|
885
|
+
const dirName = `${noteIndex}. ${title}`;
|
|
886
|
+
const encodedDirName = encodeURIComponent(dirName);
|
|
887
|
+
return `# [${dirName}](${repoUrl}/${encodedDirName})`;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// workspace/workspace.ts
|
|
891
|
+
var NOTE_CONFIG_FIELD_ORDER = [
|
|
892
|
+
"bilibili",
|
|
893
|
+
"tnotes",
|
|
894
|
+
"yuque",
|
|
895
|
+
"done",
|
|
896
|
+
"category",
|
|
897
|
+
"enableDiscussions",
|
|
898
|
+
"description",
|
|
899
|
+
"id"
|
|
900
|
+
];
|
|
901
|
+
function validateTitle(title) {
|
|
902
|
+
const value = title.trim();
|
|
903
|
+
if (!value || /[\\/\0\r\n]/.test(value) || /[. ]$/.test(value) || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(value)) {
|
|
904
|
+
throw new WorkspaceError("INVALID_TITLE", "\u7B14\u8BB0\u6216\u5206\u7EC4\u6807\u9898\u4E0D\u5408\u6CD5", {
|
|
905
|
+
title
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
return value;
|
|
909
|
+
}
|
|
910
|
+
function serializeNoteConfig(config) {
|
|
911
|
+
const record = config;
|
|
912
|
+
const sorted = {};
|
|
913
|
+
for (const field of NOTE_CONFIG_FIELD_ORDER) {
|
|
914
|
+
if (field in record) sorted[field] = record[field];
|
|
915
|
+
}
|
|
916
|
+
for (const [key, value] of Object.entries(record)) {
|
|
917
|
+
if (!(key in sorted)) sorted[key] = value;
|
|
918
|
+
}
|
|
919
|
+
return `${JSON.stringify(sorted, null, 2)}
|
|
920
|
+
`;
|
|
921
|
+
}
|
|
922
|
+
function normalizeTocContent(lines) {
|
|
923
|
+
const content = processTocEmptyLines(lines).join("\n");
|
|
924
|
+
return content.endsWith("\n") ? content : `${content}
|
|
925
|
+
`;
|
|
926
|
+
}
|
|
927
|
+
function toNoteInfos(notes) {
|
|
928
|
+
return notes.map(toNoteInfo);
|
|
929
|
+
}
|
|
930
|
+
function sidebarContent(tocLines, notes, config) {
|
|
931
|
+
const noteInfos = toNoteInfos(notes);
|
|
932
|
+
const tree = parseTocToTree(tocLines, noteInfos);
|
|
933
|
+
return JSON.stringify(
|
|
934
|
+
buildSidebarFromTocTree(tree, noteInfos, {
|
|
935
|
+
sidebarShowNoteId: config.sidebarShowNoteId ?? true,
|
|
936
|
+
sidebarIsCollapsed: true
|
|
937
|
+
}),
|
|
938
|
+
null,
|
|
939
|
+
2
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
function requireConfig(snapshot) {
|
|
943
|
+
if (snapshot.health.status === "future-schema") {
|
|
944
|
+
throw new WorkspaceError(
|
|
945
|
+
"WORKSPACE_READ_ONLY",
|
|
946
|
+
"\u77E5\u8BC6\u5E93\u7531\u66F4\u65B0\u7248\u672C\u7684 Core \u521B\u5EFA\uFF0C\u5F53\u524D\u7248\u672C\u53EA\u5141\u8BB8\u8BFB\u53D6"
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
if (snapshot.health.status !== "ready" || !snapshot.config) {
|
|
950
|
+
throw new WorkspaceError("WORKSPACE_INVALID", "\u77E5\u8BC6\u5E93\u914D\u7F6E\u5F02\u5E38\uFF0C\u7981\u6B62\u4FEE\u6539", {
|
|
951
|
+
diagnostics: snapshot.health.diagnostics
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
return snapshot.config;
|
|
955
|
+
}
|
|
956
|
+
function findNote(snapshot, noteUuid) {
|
|
957
|
+
const note = snapshot.notes.find((item) => item.uuid === noteUuid);
|
|
958
|
+
if (!note) {
|
|
959
|
+
throw new WorkspaceError("NOTE_NOT_FOUND", `\u672A\u627E\u5230\u7B14\u8BB0\uFF1A${noteUuid}`, {
|
|
960
|
+
noteUuid
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
return note;
|
|
964
|
+
}
|
|
965
|
+
function assertRevision(actual, expected, subject) {
|
|
966
|
+
if (actual !== expected) {
|
|
967
|
+
throw new WorkspaceError(
|
|
968
|
+
"REVISION_CONFLICT",
|
|
969
|
+
`${subject} \u5DF2\u88AB\u5176\u4ED6\u7A0B\u5E8F\u4FEE\u6539\uFF0C\u8BF7\u5148\u5904\u7406\u5916\u90E8\u53D8\u66F4`,
|
|
970
|
+
{ actual, expected }
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
function allocateNoteIndex(notes) {
|
|
975
|
+
const used = new Set(
|
|
976
|
+
notes.map((note) => Number.parseInt(note.index, 10)).filter((value) => Number.isInteger(value) && value >= 1 && value <= 9999)
|
|
977
|
+
);
|
|
978
|
+
for (let index = 1; index <= 9999; index++) {
|
|
979
|
+
if (!used.has(index)) return String(index).padStart(4, "0");
|
|
980
|
+
}
|
|
981
|
+
throw new WorkspaceError(
|
|
982
|
+
"NOTE_INDEX_EXHAUSTED",
|
|
983
|
+
"\u6240\u6709\u7B14\u8BB0\u7F16\u53F7\uFF080001-9999\uFF09\u5747\u5DF2\u4F7F\u7528"
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
function resolveEntryLine(lines, snapshot, entry) {
|
|
987
|
+
if (entry.type === "line") {
|
|
988
|
+
if (entry.tocLineIndex < 0 || entry.tocLineIndex >= lines.length) {
|
|
989
|
+
throw new WorkspaceError("INVALID_TOC_ENTRY", "TOC \u884C\u7D22\u5F15\u65E0\u6548", {
|
|
990
|
+
tocLineIndex: entry.tocLineIndex
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
return entry.tocLineIndex;
|
|
994
|
+
}
|
|
995
|
+
if (entry.type === "folder") {
|
|
996
|
+
try {
|
|
997
|
+
return findFolderLineIndex(lines, entry.folderPath);
|
|
998
|
+
} catch (error) {
|
|
999
|
+
throw new WorkspaceError("INVALID_TOC_ENTRY", "\u672A\u627E\u5230 TOC \u5206\u7EC4", {
|
|
1000
|
+
folderPath: entry.folderPath,
|
|
1001
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
const note = findNote(snapshot, entry.noteUuid);
|
|
1006
|
+
try {
|
|
1007
|
+
return findTocLineIndex(lines, note.index);
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
throw new WorkspaceError("INVALID_TOC_ENTRY", "\u672A\u5728 TOC \u4E2D\u627E\u5230\u7B14\u8BB0", {
|
|
1010
|
+
noteUuid: entry.noteUuid,
|
|
1011
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
function placementTargetLine(lines, snapshot, placement) {
|
|
1016
|
+
if (placement.type === "note") {
|
|
1017
|
+
return resolveEntryLine(lines, snapshot, {
|
|
1018
|
+
type: "note",
|
|
1019
|
+
noteUuid: placement.targetNoteUuid
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
return resolveEntryLine(lines, snapshot, {
|
|
1023
|
+
type: "folder",
|
|
1024
|
+
folderPath: placement.folderPath
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
function insertLineAtPlacement(lines, snapshot, placement, buildLine) {
|
|
1028
|
+
const resolvedPlacement = placement ?? { type: "root", placement: "end" };
|
|
1029
|
+
if (resolvedPlacement.type === "root") {
|
|
1030
|
+
if (resolvedPlacement.placement === "start") {
|
|
1031
|
+
const firstContent = lines.findIndex((line) => parseTocLine(line).isMatch);
|
|
1032
|
+
lines.splice(firstContent >= 0 ? firstContent : lines.length, 0, buildLine(0));
|
|
1033
|
+
} else {
|
|
1034
|
+
let insertAt = lines.length;
|
|
1035
|
+
while (insertAt > 0 && lines[insertAt - 1] === "") insertAt--;
|
|
1036
|
+
lines.splice(insertAt, 0, buildLine(0));
|
|
1037
|
+
}
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
const targetLine = placementTargetLine(lines, snapshot, resolvedPlacement);
|
|
1041
|
+
const target = parseTocLine(lines[targetLine]);
|
|
1042
|
+
if (!target.isMatch) {
|
|
1043
|
+
throw new WorkspaceError("INVALID_TOC_ENTRY", "\u76EE\u6807 TOC \u6761\u76EE\u65E0\u6548");
|
|
1044
|
+
}
|
|
1045
|
+
if (resolvedPlacement.placement === "inside") {
|
|
1046
|
+
const range = getTocEntrySubtreeRange(lines, targetLine);
|
|
1047
|
+
lines.splice(range.end, 0, buildLine(target.indentLevel + 1));
|
|
1048
|
+
} else if (resolvedPlacement.placement === "before") {
|
|
1049
|
+
lines.splice(targetLine, 0, buildLine(target.indentLevel));
|
|
1050
|
+
} else {
|
|
1051
|
+
const range = getTocEntrySubtreeRange(lines, targetLine);
|
|
1052
|
+
lines.splice(range.end, 0, buildLine(target.indentLevel));
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
function adjustIndent(lines, delta) {
|
|
1056
|
+
return lines.map((line) => {
|
|
1057
|
+
const parsed = parseTocLine(line);
|
|
1058
|
+
if (!parsed.isMatch) return line;
|
|
1059
|
+
const indent = Math.max(0, parsed.indentLevel + delta);
|
|
1060
|
+
return `${" ".repeat(indent * TOC_INDENT_SPACES)}${line.trimStart()}`;
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
async function listFilesRecursively(directoryPath) {
|
|
1064
|
+
const result = [];
|
|
1065
|
+
const entries = await fs3.readdir(directoryPath, { withFileTypes: true });
|
|
1066
|
+
for (const entry of entries) {
|
|
1067
|
+
const entryPath = path4.join(directoryPath, entry.name);
|
|
1068
|
+
if (entry.isDirectory()) {
|
|
1069
|
+
result.push(...await listFilesRecursively(entryPath));
|
|
1070
|
+
} else {
|
|
1071
|
+
result.push(entryPath);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return result;
|
|
1075
|
+
}
|
|
1076
|
+
var Workspace = class {
|
|
1077
|
+
constructor(options) {
|
|
1078
|
+
this.queue = new MutationQueue();
|
|
1079
|
+
this.disposed = false;
|
|
1080
|
+
this.paths = createWorkspacePaths(options.rootPath);
|
|
1081
|
+
this.logger = options.logger ?? {};
|
|
1082
|
+
this.prettierByDefault = options.format?.prettier ?? true;
|
|
1083
|
+
this.notes = {
|
|
1084
|
+
read: (noteUuid) => this.readNote(noteUuid),
|
|
1085
|
+
save: (input) => this.saveNote(input),
|
|
1086
|
+
create: (input) => this.createNote(input),
|
|
1087
|
+
rename: (input) => this.renameNote(input),
|
|
1088
|
+
updateConfig: (input) => this.updateNoteConfig(input)
|
|
1089
|
+
};
|
|
1090
|
+
this.toc = {
|
|
1091
|
+
move: (input) => this.moveTocEntry(input),
|
|
1092
|
+
createGroup: (input) => this.createTocGroup(input),
|
|
1093
|
+
renameGroup: (input) => this.renameTocGroup(input),
|
|
1094
|
+
previewDelete: (entry) => this.previewDelete(entry),
|
|
1095
|
+
deleteEntry: (input) => this.deleteTocEntry(input),
|
|
1096
|
+
setDone: (input) => this.updateNoteConfig(input),
|
|
1097
|
+
reconcileFromFiles: () => this.reconcileTocFromFiles()
|
|
1098
|
+
};
|
|
1099
|
+
this.attachments = {
|
|
1100
|
+
writeLocal: (input) => this.writeLocalAttachment(input)
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
async inspect() {
|
|
1104
|
+
this.assertActive();
|
|
1105
|
+
return (await scanWorkspace(this.paths)).snapshot;
|
|
1106
|
+
}
|
|
1107
|
+
async refresh() {
|
|
1108
|
+
return this.inspect();
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Files-first TOC reconcile (0004): align TOC.md + sidebar.json with the
|
|
1112
|
+
* disk truth. Valid notes missing from the TOC are appended at root level
|
|
1113
|
+
* (by index); note dirs whose config is missing/invalid are soft-deleted to
|
|
1114
|
+
* notes/.trash/. Idempotent: runs again without changes -> no file writes.
|
|
1115
|
+
*/
|
|
1116
|
+
async reconcileTocFromFiles() {
|
|
1117
|
+
return this.queue.run(async () => {
|
|
1118
|
+
this.assertActive();
|
|
1119
|
+
const scanned = await scanWorkspace(this.paths);
|
|
1120
|
+
const { snapshot } = scanned;
|
|
1121
|
+
if (!snapshot.config) {
|
|
1122
|
+
throw new WorkspaceError(
|
|
1123
|
+
"WORKSPACE_INVALID",
|
|
1124
|
+
"\u77E5\u8BC6\u5E93\u914D\u7F6E\u5F02\u5E38\uFF0C\u7981\u6B62\u4FEE\u6539",
|
|
1125
|
+
{ diagnostics: snapshot.health.diagnostics }
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
const plan = planReconcile(snapshot);
|
|
1129
|
+
const changedFiles = [];
|
|
1130
|
+
if (plan.trashDirs.length > 0) {
|
|
1131
|
+
const trashDir = path4.join(this.paths.notes, ".trash");
|
|
1132
|
+
await fs3.mkdir(trashDir, { recursive: true });
|
|
1133
|
+
for (const dir of plan.trashDirs) {
|
|
1134
|
+
const source = path4.join(this.paths.notes, dir);
|
|
1135
|
+
let target = path4.join(trashDir, dir);
|
|
1136
|
+
try {
|
|
1137
|
+
await fs3.stat(target);
|
|
1138
|
+
target = path4.join(trashDir, `${dir}-${Date.now()}`);
|
|
1139
|
+
} catch {
|
|
1140
|
+
}
|
|
1141
|
+
await fs3.rename(source, target);
|
|
1142
|
+
changedFiles.push({ path: target, previousPath: source, kind: "trashed" });
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
const noteInfos = snapshot.notes.map(toNoteInfo);
|
|
1146
|
+
const configByIndex = new Map(
|
|
1147
|
+
snapshot.notes.map((note) => [note.index, { done: note.config.done }])
|
|
1148
|
+
);
|
|
1149
|
+
const lines = serializeTocTree(plan.tree, noteInfos, configByIndex);
|
|
1150
|
+
const nextToc = normalizeTocContent(lines);
|
|
1151
|
+
if (nextToc !== (scanned.tocText ?? "")) {
|
|
1152
|
+
changedFiles.push({ path: this.paths.toc, kind: "updated" });
|
|
1153
|
+
await writeFilesAtomically([{ path: this.paths.toc, data: nextToc }]);
|
|
1154
|
+
}
|
|
1155
|
+
const finalTocTree = parseTocToTree(
|
|
1156
|
+
nextToc.split("\n"),
|
|
1157
|
+
noteInfos
|
|
1158
|
+
);
|
|
1159
|
+
const sidebar = buildSidebarFromTocTree(finalTocTree, noteInfos, {
|
|
1160
|
+
sidebarShowNoteId: snapshot.config.sidebarShowNoteId ?? true,
|
|
1161
|
+
sidebarIsCollapsed: true
|
|
1162
|
+
});
|
|
1163
|
+
const nextSidebar = `${JSON.stringify(sidebar, null, 2)}
|
|
1164
|
+
`;
|
|
1165
|
+
let currentSidebar = "";
|
|
1166
|
+
try {
|
|
1167
|
+
currentSidebar = await fs3.readFile(this.paths.sidebar, "utf-8");
|
|
1168
|
+
} catch {
|
|
1169
|
+
}
|
|
1170
|
+
if (nextSidebar !== currentSidebar) {
|
|
1171
|
+
changedFiles.push({ path: this.paths.sidebar, kind: "updated" });
|
|
1172
|
+
await writeFilesAtomically([{ path: this.paths.sidebar, data: nextSidebar }]);
|
|
1173
|
+
}
|
|
1174
|
+
const refreshed = await this.inspect();
|
|
1175
|
+
return {
|
|
1176
|
+
value: refreshed,
|
|
1177
|
+
changedFiles,
|
|
1178
|
+
snapshotRevision: refreshed.revision
|
|
1179
|
+
};
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
async reconcileTocCompletion() {
|
|
1183
|
+
return this.queue.run(async () => {
|
|
1184
|
+
const scanned = await this.scanReady();
|
|
1185
|
+
const { snapshot, tocText } = scanned;
|
|
1186
|
+
const config = requireConfig(snapshot);
|
|
1187
|
+
const lines = (tocText ?? "").split("\n");
|
|
1188
|
+
const completedByIndex = /* @__PURE__ */ new Map();
|
|
1189
|
+
for (const line of lines) {
|
|
1190
|
+
const parsed = parseTocLine(line);
|
|
1191
|
+
if (parsed.noteIndex && !completedByIndex.has(parsed.noteIndex)) {
|
|
1192
|
+
completedByIndex.set(parsed.noteIndex, parsed.completed);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
const changedFiles = [];
|
|
1196
|
+
const writes = [];
|
|
1197
|
+
const updatedNotes = snapshot.notes.map((note) => {
|
|
1198
|
+
const completed = completedByIndex.get(note.index);
|
|
1199
|
+
if (completed === void 0 || completed === note.config.done) return note;
|
|
1200
|
+
const updatedConfig = { ...note.config, done: completed };
|
|
1201
|
+
writes.push({ path: note.configPath, data: serializeNoteConfig(updatedConfig) });
|
|
1202
|
+
changedFiles.push({ path: note.configPath, kind: "updated" });
|
|
1203
|
+
return { ...note, config: updatedConfig };
|
|
1204
|
+
});
|
|
1205
|
+
if (writes.length > 0) {
|
|
1206
|
+
writes.push({
|
|
1207
|
+
path: this.paths.sidebar,
|
|
1208
|
+
data: sidebarContent(lines, updatedNotes, config)
|
|
1209
|
+
});
|
|
1210
|
+
changedFiles.push({ path: this.paths.sidebar, kind: "updated" });
|
|
1211
|
+
await writeFilesAtomically(writes);
|
|
1212
|
+
}
|
|
1213
|
+
const value = await this.inspect();
|
|
1214
|
+
return { value, changedFiles, snapshotRevision: value.revision };
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
async dispose() {
|
|
1218
|
+
if (this.disposed) return;
|
|
1219
|
+
this.disposed = true;
|
|
1220
|
+
await this.queue.dispose();
|
|
1221
|
+
}
|
|
1222
|
+
assertActive() {
|
|
1223
|
+
if (this.disposed) {
|
|
1224
|
+
throw new WorkspaceError("WORKSPACE_DISPOSED", "\u5DE5\u4F5C\u533A\u5B9E\u4F8B\u5DF2\u7ECF\u91CA\u653E");
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
async scanReady() {
|
|
1228
|
+
this.assertActive();
|
|
1229
|
+
const scanned = await scanWorkspace(this.paths);
|
|
1230
|
+
requireConfig(scanned.snapshot);
|
|
1231
|
+
return scanned;
|
|
1232
|
+
}
|
|
1233
|
+
async readNote(noteUuid) {
|
|
1234
|
+
const snapshot = await this.inspect();
|
|
1235
|
+
const note = findNote(snapshot, noteUuid);
|
|
1236
|
+
return { ...note, content: await fs3.readFile(note.readmePath, "utf8") };
|
|
1237
|
+
}
|
|
1238
|
+
async saveNote(input) {
|
|
1239
|
+
return this.queue.run(async () => {
|
|
1240
|
+
const { snapshot } = await this.scanReady();
|
|
1241
|
+
const config = requireConfig(snapshot);
|
|
1242
|
+
const note = findNote(snapshot, input.noteUuid);
|
|
1243
|
+
assertRevision(note.revision, input.expectedRevision, "\u7B14\u8BB0");
|
|
1244
|
+
const formatted = await formatTNotesNote({
|
|
1245
|
+
content: input.content,
|
|
1246
|
+
noteIndex: note.index,
|
|
1247
|
+
title: note.title,
|
|
1248
|
+
repoOwner: config.author,
|
|
1249
|
+
repoName: config.repoName,
|
|
1250
|
+
noteConfig: note.config,
|
|
1251
|
+
prettier: input.prettier ?? this.prettierByDefault
|
|
1252
|
+
});
|
|
1253
|
+
const current = await fs3.readFile(note.readmePath, "utf8");
|
|
1254
|
+
const changedFiles = [];
|
|
1255
|
+
if (formatted.content !== current) {
|
|
1256
|
+
await writeFilesAtomically([
|
|
1257
|
+
{ path: note.readmePath, data: formatted.content }
|
|
1258
|
+
]);
|
|
1259
|
+
changedFiles.push({ path: note.readmePath, kind: "updated" });
|
|
1260
|
+
}
|
|
1261
|
+
const value = await this.readNote(input.noteUuid);
|
|
1262
|
+
const refreshed = await this.inspect();
|
|
1263
|
+
return { value, changedFiles, snapshotRevision: refreshed.revision };
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
async createNote(input) {
|
|
1267
|
+
return this.queue.run(async () => {
|
|
1268
|
+
const { snapshot, tocText } = await this.scanReady();
|
|
1269
|
+
const config = requireConfig(snapshot);
|
|
1270
|
+
if (input.expectedSnapshotRevision) {
|
|
1271
|
+
assertRevision(
|
|
1272
|
+
snapshot.revision,
|
|
1273
|
+
input.expectedSnapshotRevision,
|
|
1274
|
+
"\u77E5\u8BC6\u5E93\u76EE\u5F55"
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
const title = validateTitle(input.title);
|
|
1278
|
+
const index = allocateNoteIndex(snapshot.notes);
|
|
1279
|
+
const dirName = `${index}. ${title}`;
|
|
1280
|
+
const directoryPath = path4.join(this.paths.notes, dirName);
|
|
1281
|
+
assertPathInside(this.paths.notes, directoryPath);
|
|
1282
|
+
try {
|
|
1283
|
+
await fs3.mkdir(directoryPath);
|
|
1284
|
+
} catch (error) {
|
|
1285
|
+
throw new WorkspaceError("FILESYSTEM_ERROR", "\u65E0\u6CD5\u521B\u5EFA\u7B14\u8BB0\u76EE\u5F55", {
|
|
1286
|
+
directoryPath,
|
|
1287
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
const noteConfig = {
|
|
1291
|
+
bilibili: [],
|
|
1292
|
+
tnotes: [],
|
|
1293
|
+
yuque: [],
|
|
1294
|
+
done: false,
|
|
1295
|
+
enableDiscussions: input.config?.enableDiscussions ?? false,
|
|
1296
|
+
description: input.config?.description ?? "",
|
|
1297
|
+
id: randomUUID2()
|
|
1298
|
+
};
|
|
1299
|
+
const readmePath = path4.join(directoryPath, "README.md");
|
|
1300
|
+
const configPath = path4.join(directoryPath, ".tnotes.json");
|
|
1301
|
+
const formatted = await formatTNotesNote({
|
|
1302
|
+
content: getNewNoteReadmeBody(),
|
|
1303
|
+
noteIndex: index,
|
|
1304
|
+
title,
|
|
1305
|
+
repoOwner: config.author,
|
|
1306
|
+
repoName: config.repoName,
|
|
1307
|
+
noteConfig,
|
|
1308
|
+
prettier: this.prettierByDefault
|
|
1309
|
+
});
|
|
1310
|
+
const newNote = {
|
|
1311
|
+
uuid: noteConfig.id,
|
|
1312
|
+
index,
|
|
1313
|
+
title,
|
|
1314
|
+
dirName,
|
|
1315
|
+
directoryPath,
|
|
1316
|
+
readmePath,
|
|
1317
|
+
configPath,
|
|
1318
|
+
config: noteConfig,
|
|
1319
|
+
revision: ""
|
|
1320
|
+
};
|
|
1321
|
+
const tocLines = (tocText ?? "").split("\n");
|
|
1322
|
+
insertLineAtPlacement(
|
|
1323
|
+
tocLines,
|
|
1324
|
+
snapshot,
|
|
1325
|
+
input.placement,
|
|
1326
|
+
(indent) => buildTocLine(toNoteInfo(newNote), indent, false)
|
|
1327
|
+
);
|
|
1328
|
+
const updatedNotes = [...snapshot.notes, newNote];
|
|
1329
|
+
const normalizedToc = normalizeTocContent(tocLines);
|
|
1330
|
+
const normalizedLines = normalizedToc.split("\n");
|
|
1331
|
+
try {
|
|
1332
|
+
await writeFilesAtomically([
|
|
1333
|
+
{ path: readmePath, data: formatted.content },
|
|
1334
|
+
{ path: configPath, data: serializeNoteConfig(noteConfig) },
|
|
1335
|
+
{ path: this.paths.toc, data: normalizedToc },
|
|
1336
|
+
{
|
|
1337
|
+
path: this.paths.sidebar,
|
|
1338
|
+
data: sidebarContent(normalizedLines, updatedNotes, config)
|
|
1339
|
+
}
|
|
1340
|
+
]);
|
|
1341
|
+
} catch (error) {
|
|
1342
|
+
await fs3.rm(directoryPath, { recursive: true, force: true });
|
|
1343
|
+
throw error;
|
|
1344
|
+
}
|
|
1345
|
+
const value = await this.readNote(noteConfig.id);
|
|
1346
|
+
const refreshed = await this.inspect();
|
|
1347
|
+
return {
|
|
1348
|
+
value,
|
|
1349
|
+
changedFiles: [
|
|
1350
|
+
{ path: directoryPath, kind: "created" },
|
|
1351
|
+
{ path: readmePath, kind: "created" },
|
|
1352
|
+
{ path: configPath, kind: "created" },
|
|
1353
|
+
{ path: this.paths.toc, kind: "updated" },
|
|
1354
|
+
{ path: this.paths.sidebar, kind: "updated" }
|
|
1355
|
+
],
|
|
1356
|
+
snapshotRevision: refreshed.revision
|
|
1357
|
+
};
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
async renameNote(input) {
|
|
1361
|
+
return this.queue.run(async () => {
|
|
1362
|
+
const { snapshot, tocText } = await this.scanReady();
|
|
1363
|
+
const config = requireConfig(snapshot);
|
|
1364
|
+
const note = findNote(snapshot, input.noteUuid);
|
|
1365
|
+
assertRevision(note.revision, input.expectedRevision, "\u7B14\u8BB0");
|
|
1366
|
+
const title = validateTitle(input.title);
|
|
1367
|
+
const newDirName = `${note.index}. ${title}`;
|
|
1368
|
+
if (newDirName === note.dirName) {
|
|
1369
|
+
const value2 = await this.readNote(note.uuid);
|
|
1370
|
+
return { value: value2, changedFiles: [], snapshotRevision: snapshot.revision };
|
|
1371
|
+
}
|
|
1372
|
+
const newDirectoryPath = path4.join(this.paths.notes, newDirName);
|
|
1373
|
+
assertPathInside(this.paths.notes, newDirectoryPath);
|
|
1374
|
+
try {
|
|
1375
|
+
await fs3.access(newDirectoryPath);
|
|
1376
|
+
throw new WorkspaceError("INVALID_TITLE", "\u76EE\u6807\u7B14\u8BB0\u76EE\u5F55\u5DF2\u7ECF\u5B58\u5728", {
|
|
1377
|
+
newDirectoryPath
|
|
1378
|
+
});
|
|
1379
|
+
} catch (error) {
|
|
1380
|
+
if (error instanceof WorkspaceError) throw error;
|
|
1381
|
+
}
|
|
1382
|
+
const currentContent = await fs3.readFile(note.readmePath, "utf8");
|
|
1383
|
+
const formatted = await formatTNotesNote({
|
|
1384
|
+
content: currentContent,
|
|
1385
|
+
noteIndex: note.index,
|
|
1386
|
+
title,
|
|
1387
|
+
repoOwner: config.author,
|
|
1388
|
+
repoName: config.repoName,
|
|
1389
|
+
noteConfig: note.config,
|
|
1390
|
+
prettier: this.prettierByDefault
|
|
1391
|
+
});
|
|
1392
|
+
const renamedNote = {
|
|
1393
|
+
...note,
|
|
1394
|
+
title,
|
|
1395
|
+
dirName: newDirName,
|
|
1396
|
+
directoryPath: newDirectoryPath,
|
|
1397
|
+
readmePath: path4.join(newDirectoryPath, "README.md"),
|
|
1398
|
+
configPath: path4.join(newDirectoryPath, ".tnotes.json")
|
|
1399
|
+
};
|
|
1400
|
+
const tocLines = (tocText ?? "").split("\n");
|
|
1401
|
+
for (let index = 0; index < tocLines.length; index++) {
|
|
1402
|
+
const parsed = parseTocLine(tocLines[index]);
|
|
1403
|
+
if (parsed.noteIndex === note.index) {
|
|
1404
|
+
tocLines[index] = buildTocLine(
|
|
1405
|
+
toNoteInfo(renamedNote),
|
|
1406
|
+
parsed.indentLevel,
|
|
1407
|
+
parsed.completed
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
const normalizedToc = normalizeTocContent(tocLines);
|
|
1412
|
+
const updatedNotes = snapshot.notes.map(
|
|
1413
|
+
(item) => item.uuid === note.uuid ? renamedNote : item
|
|
1414
|
+
);
|
|
1415
|
+
await fs3.rename(note.directoryPath, newDirectoryPath);
|
|
1416
|
+
try {
|
|
1417
|
+
await writeFilesAtomically([
|
|
1418
|
+
{ path: renamedNote.readmePath, data: formatted.content },
|
|
1419
|
+
{ path: this.paths.toc, data: normalizedToc },
|
|
1420
|
+
{
|
|
1421
|
+
path: this.paths.sidebar,
|
|
1422
|
+
data: sidebarContent(normalizedToc.split("\n"), updatedNotes, config)
|
|
1423
|
+
}
|
|
1424
|
+
]);
|
|
1425
|
+
} catch (error) {
|
|
1426
|
+
await fs3.rename(newDirectoryPath, note.directoryPath);
|
|
1427
|
+
throw error;
|
|
1428
|
+
}
|
|
1429
|
+
const value = await this.readNote(note.uuid);
|
|
1430
|
+
const refreshed = await this.inspect();
|
|
1431
|
+
return {
|
|
1432
|
+
value,
|
|
1433
|
+
changedFiles: [
|
|
1434
|
+
{
|
|
1435
|
+
path: newDirectoryPath,
|
|
1436
|
+
previousPath: note.directoryPath,
|
|
1437
|
+
kind: "renamed"
|
|
1438
|
+
},
|
|
1439
|
+
{ path: renamedNote.readmePath, kind: "updated" },
|
|
1440
|
+
{ path: this.paths.toc, kind: "updated" },
|
|
1441
|
+
{ path: this.paths.sidebar, kind: "updated" }
|
|
1442
|
+
],
|
|
1443
|
+
snapshotRevision: refreshed.revision
|
|
1444
|
+
};
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
async updateNoteConfig(input) {
|
|
1448
|
+
return this.queue.run(async () => {
|
|
1449
|
+
const { snapshot, tocText } = await this.scanReady();
|
|
1450
|
+
const config = requireConfig(snapshot);
|
|
1451
|
+
const note = findNote(snapshot, input.noteUuid);
|
|
1452
|
+
assertRevision(note.revision, input.expectedRevision, "\u7B14\u8BB0");
|
|
1453
|
+
const updatedConfig = { ...note.config, ...input.updates };
|
|
1454
|
+
const writes = [
|
|
1455
|
+
{ path: note.configPath, data: serializeNoteConfig(updatedConfig) }
|
|
1456
|
+
];
|
|
1457
|
+
const changedFiles = [
|
|
1458
|
+
{ path: note.configPath, kind: "updated" }
|
|
1459
|
+
];
|
|
1460
|
+
if (typeof input.updates.done === "boolean") {
|
|
1461
|
+
const lines = (tocText ?? "").split("\n");
|
|
1462
|
+
let updated = false;
|
|
1463
|
+
const tempNote = { ...note, config: updatedConfig };
|
|
1464
|
+
for (let index = 0; index < lines.length; index++) {
|
|
1465
|
+
const parsed = parseTocLine(lines[index]);
|
|
1466
|
+
if (parsed.noteIndex === note.index) {
|
|
1467
|
+
lines[index] = buildTocLine(
|
|
1468
|
+
toNoteInfo(tempNote),
|
|
1469
|
+
parsed.indentLevel,
|
|
1470
|
+
input.updates.done
|
|
1471
|
+
);
|
|
1472
|
+
updated = true;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
if (updated) {
|
|
1476
|
+
const normalizedToc = normalizeTocContent(lines);
|
|
1477
|
+
const updatedNotes = snapshot.notes.map(
|
|
1478
|
+
(item) => item.uuid === note.uuid ? tempNote : item
|
|
1479
|
+
);
|
|
1480
|
+
writes.push(
|
|
1481
|
+
{ path: this.paths.toc, data: normalizedToc },
|
|
1482
|
+
{
|
|
1483
|
+
path: this.paths.sidebar,
|
|
1484
|
+
data: sidebarContent(normalizedToc.split("\n"), updatedNotes, config)
|
|
1485
|
+
}
|
|
1486
|
+
);
|
|
1487
|
+
changedFiles.push(
|
|
1488
|
+
{ path: this.paths.toc, kind: "updated" },
|
|
1489
|
+
{ path: this.paths.sidebar, kind: "updated" }
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
await writeFilesAtomically(writes);
|
|
1494
|
+
const value = await this.readNote(note.uuid);
|
|
1495
|
+
const refreshed = await this.inspect();
|
|
1496
|
+
return { value, changedFiles, snapshotRevision: refreshed.revision };
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
async moveTocEntry(input) {
|
|
1500
|
+
return this.queue.run(async () => {
|
|
1501
|
+
const { snapshot, tocText } = await this.scanReady();
|
|
1502
|
+
const config = requireConfig(snapshot);
|
|
1503
|
+
assertRevision(snapshot.revision, input.expectedSnapshotRevision, "\u77E5\u8BC6\u5E93\u76EE\u5F55");
|
|
1504
|
+
const lines = (tocText ?? "").split("\n");
|
|
1505
|
+
const sourceLine = resolveEntryLine(lines, snapshot, input.source);
|
|
1506
|
+
const targetLine = resolveEntryLine(lines, snapshot, input.target);
|
|
1507
|
+
const sourceRange = getTocEntrySubtreeRange(lines, sourceLine);
|
|
1508
|
+
if (targetLine >= sourceRange.start && targetLine < sourceRange.end) {
|
|
1509
|
+
throw new WorkspaceError(
|
|
1510
|
+
"INVALID_TOC_ENTRY",
|
|
1511
|
+
"\u4E0D\u80FD\u628A\u76EE\u5F55\u6761\u76EE\u79FB\u52A8\u5230\u81EA\u8EAB\u6216\u81EA\u8EAB\u5B50\u6811\u5185"
|
|
1512
|
+
);
|
|
1513
|
+
}
|
|
1514
|
+
const moving = lines.splice(
|
|
1515
|
+
sourceRange.start,
|
|
1516
|
+
sourceRange.end - sourceRange.start
|
|
1517
|
+
);
|
|
1518
|
+
const adjustedTarget = adjustTocLineIndexAfterSubtreeRemoval(
|
|
1519
|
+
targetLine,
|
|
1520
|
+
sourceRange.start,
|
|
1521
|
+
sourceRange.end
|
|
1522
|
+
);
|
|
1523
|
+
const target = parseTocLine(lines[adjustedTarget]);
|
|
1524
|
+
if (!target.isMatch) {
|
|
1525
|
+
throw new WorkspaceError("INVALID_TOC_ENTRY", "\u79FB\u52A8\u76EE\u6807\u65E0\u6548");
|
|
1526
|
+
}
|
|
1527
|
+
let insertAt;
|
|
1528
|
+
let indent;
|
|
1529
|
+
if (input.placement === "inside") {
|
|
1530
|
+
insertAt = getTocEntrySubtreeRange(lines, adjustedTarget).end;
|
|
1531
|
+
indent = target.indentLevel + 1;
|
|
1532
|
+
} else if (input.placement === "before") {
|
|
1533
|
+
insertAt = adjustedTarget;
|
|
1534
|
+
indent = target.indentLevel;
|
|
1535
|
+
} else {
|
|
1536
|
+
insertAt = getTocEntrySubtreeRange(lines, adjustedTarget).end;
|
|
1537
|
+
indent = target.indentLevel;
|
|
1538
|
+
}
|
|
1539
|
+
const oldIndent = parseTocLine(moving[0]).indentLevel;
|
|
1540
|
+
lines.splice(insertAt, 0, ...adjustIndent(moving, indent - oldIndent));
|
|
1541
|
+
const normalizedToc = normalizeTocContent(lines);
|
|
1542
|
+
await writeFilesAtomically([
|
|
1543
|
+
{ path: this.paths.toc, data: normalizedToc },
|
|
1544
|
+
{
|
|
1545
|
+
path: this.paths.sidebar,
|
|
1546
|
+
data: sidebarContent(normalizedToc.split("\n"), snapshot.notes, config)
|
|
1547
|
+
}
|
|
1548
|
+
]);
|
|
1549
|
+
const value = await this.inspect();
|
|
1550
|
+
return {
|
|
1551
|
+
value,
|
|
1552
|
+
changedFiles: [
|
|
1553
|
+
{ path: this.paths.toc, kind: "updated" },
|
|
1554
|
+
{ path: this.paths.sidebar, kind: "updated" }
|
|
1555
|
+
],
|
|
1556
|
+
snapshotRevision: value.revision
|
|
1557
|
+
};
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
async createTocGroup(input) {
|
|
1561
|
+
return this.queue.run(async () => {
|
|
1562
|
+
const { snapshot, tocText } = await this.scanReady();
|
|
1563
|
+
const config = requireConfig(snapshot);
|
|
1564
|
+
assertRevision(snapshot.revision, input.expectedSnapshotRevision, "\u77E5\u8BC6\u5E93\u76EE\u5F55");
|
|
1565
|
+
const title = validateTitle(input.title);
|
|
1566
|
+
const lines = (tocText ?? "").split("\n");
|
|
1567
|
+
insertLineAtPlacement(
|
|
1568
|
+
lines,
|
|
1569
|
+
snapshot,
|
|
1570
|
+
input.placement,
|
|
1571
|
+
(indent) => buildFolderTocLine(title, indent)
|
|
1572
|
+
);
|
|
1573
|
+
const normalizedToc = normalizeTocContent(lines);
|
|
1574
|
+
await writeFilesAtomically([
|
|
1575
|
+
{ path: this.paths.toc, data: normalizedToc },
|
|
1576
|
+
{
|
|
1577
|
+
path: this.paths.sidebar,
|
|
1578
|
+
data: sidebarContent(normalizedToc.split("\n"), snapshot.notes, config)
|
|
1579
|
+
}
|
|
1580
|
+
]);
|
|
1581
|
+
const value = await this.inspect();
|
|
1582
|
+
return {
|
|
1583
|
+
value,
|
|
1584
|
+
changedFiles: [
|
|
1585
|
+
{ path: this.paths.toc, kind: "updated" },
|
|
1586
|
+
{ path: this.paths.sidebar, kind: "updated" }
|
|
1587
|
+
],
|
|
1588
|
+
snapshotRevision: value.revision
|
|
1589
|
+
};
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
async renameTocGroup(input) {
|
|
1593
|
+
return this.queue.run(async () => {
|
|
1594
|
+
const { snapshot, tocText } = await this.scanReady();
|
|
1595
|
+
const config = requireConfig(snapshot);
|
|
1596
|
+
assertRevision(snapshot.revision, input.expectedSnapshotRevision, "\u77E5\u8BC6\u5E93\u76EE\u5F55");
|
|
1597
|
+
const lines = (tocText ?? "").split("\n");
|
|
1598
|
+
const lineIndex = resolveEntryLine(lines, snapshot, {
|
|
1599
|
+
type: "folder",
|
|
1600
|
+
folderPath: input.folderPath
|
|
1601
|
+
});
|
|
1602
|
+
const updatedLines = renameFolderLine(lines, lineIndex, validateTitle(input.title));
|
|
1603
|
+
const normalizedToc = normalizeTocContent(updatedLines);
|
|
1604
|
+
await writeFilesAtomically([
|
|
1605
|
+
{ path: this.paths.toc, data: normalizedToc },
|
|
1606
|
+
{
|
|
1607
|
+
path: this.paths.sidebar,
|
|
1608
|
+
data: sidebarContent(normalizedToc.split("\n"), snapshot.notes, config)
|
|
1609
|
+
}
|
|
1610
|
+
]);
|
|
1611
|
+
const value = await this.inspect();
|
|
1612
|
+
return {
|
|
1613
|
+
value,
|
|
1614
|
+
changedFiles: [
|
|
1615
|
+
{ path: this.paths.toc, kind: "updated" },
|
|
1616
|
+
{ path: this.paths.sidebar, kind: "updated" }
|
|
1617
|
+
],
|
|
1618
|
+
snapshotRevision: value.revision
|
|
1619
|
+
};
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
async previewDelete(entry) {
|
|
1623
|
+
const { snapshot, tocText } = await this.scanReady();
|
|
1624
|
+
const lines = (tocText ?? "").split("\n");
|
|
1625
|
+
const lineIndex = resolveEntryLine(lines, snapshot, entry);
|
|
1626
|
+
const indexes = collectNoteIndexesInSubtree(lines, lineIndex);
|
|
1627
|
+
const notes = indexes.map((index) => snapshot.notes.find((note) => note.index === index)).filter((note) => Boolean(note));
|
|
1628
|
+
const filePaths = (await Promise.all(notes.map((note) => listFilesRecursively(note.directoryPath)))).flat();
|
|
1629
|
+
return {
|
|
1630
|
+
entry,
|
|
1631
|
+
notes: notes.map((note) => ({
|
|
1632
|
+
noteUuid: note.uuid,
|
|
1633
|
+
index: note.index,
|
|
1634
|
+
title: note.title,
|
|
1635
|
+
directoryPath: note.directoryPath
|
|
1636
|
+
})),
|
|
1637
|
+
filePaths,
|
|
1638
|
+
directoryPaths: notes.map((note) => note.directoryPath),
|
|
1639
|
+
snapshotRevision: snapshot.revision
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
async deleteTocEntry(input) {
|
|
1643
|
+
return this.queue.run(async () => {
|
|
1644
|
+
const { snapshot, tocText } = await this.scanReady();
|
|
1645
|
+
const config = requireConfig(snapshot);
|
|
1646
|
+
assertRevision(snapshot.revision, input.expectedSnapshotRevision, "\u77E5\u8BC6\u5E93\u76EE\u5F55");
|
|
1647
|
+
const lines = (tocText ?? "").split("\n");
|
|
1648
|
+
const lineIndex = resolveEntryLine(lines, snapshot, input.entry);
|
|
1649
|
+
const range = getTocEntrySubtreeRange(lines, lineIndex);
|
|
1650
|
+
const indexes = collectNoteIndexesInSubtree(lines, lineIndex);
|
|
1651
|
+
const notes = indexes.map((index) => snapshot.notes.find((note) => note.index === index)).filter((note) => Boolean(note));
|
|
1652
|
+
lines.splice(range.start, range.end - range.start);
|
|
1653
|
+
const remainingNotes = snapshot.notes.filter(
|
|
1654
|
+
(note) => !indexes.includes(note.index)
|
|
1655
|
+
);
|
|
1656
|
+
const normalizedToc = normalizeTocContent(lines);
|
|
1657
|
+
const movedDirectories = [];
|
|
1658
|
+
try {
|
|
1659
|
+
for (const note of notes) {
|
|
1660
|
+
const temporary = path4.join(
|
|
1661
|
+
this.paths.notes,
|
|
1662
|
+
`.${path4.basename(note.directoryPath)}.desk-delete-${randomUUID2()}`
|
|
1663
|
+
);
|
|
1664
|
+
await fs3.rename(note.directoryPath, temporary);
|
|
1665
|
+
movedDirectories.push({ original: note.directoryPath, temporary });
|
|
1666
|
+
}
|
|
1667
|
+
await writeFilesAtomically([
|
|
1668
|
+
{ path: this.paths.toc, data: normalizedToc },
|
|
1669
|
+
{
|
|
1670
|
+
path: this.paths.sidebar,
|
|
1671
|
+
data: sidebarContent(normalizedToc.split("\n"), remainingNotes, config)
|
|
1672
|
+
}
|
|
1673
|
+
]);
|
|
1674
|
+
} catch (error) {
|
|
1675
|
+
for (const moved of movedDirectories.reverse()) {
|
|
1676
|
+
await fs3.rename(moved.temporary, moved.original);
|
|
1677
|
+
}
|
|
1678
|
+
throw error;
|
|
1679
|
+
}
|
|
1680
|
+
for (const moved of movedDirectories) {
|
|
1681
|
+
await fs3.rm(moved.temporary, { recursive: true, force: true });
|
|
1682
|
+
}
|
|
1683
|
+
const value = await this.inspect();
|
|
1684
|
+
return {
|
|
1685
|
+
value,
|
|
1686
|
+
changedFiles: [
|
|
1687
|
+
...notes.map((note) => ({
|
|
1688
|
+
path: note.directoryPath,
|
|
1689
|
+
kind: "deleted"
|
|
1690
|
+
})),
|
|
1691
|
+
{ path: this.paths.toc, kind: "updated" },
|
|
1692
|
+
{ path: this.paths.sidebar, kind: "updated" }
|
|
1693
|
+
],
|
|
1694
|
+
snapshotRevision: value.revision
|
|
1695
|
+
};
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
async writeLocalAttachment(input) {
|
|
1699
|
+
return this.queue.run(async () => {
|
|
1700
|
+
const { snapshot } = await this.scanReady();
|
|
1701
|
+
const note = findNote(snapshot, input.noteUuid);
|
|
1702
|
+
const assetsPath = path4.join(note.directoryPath, "assets");
|
|
1703
|
+
assertPathInside(note.directoryPath, assetsPath);
|
|
1704
|
+
const requestedName = sanitizeFileName(input.fileName);
|
|
1705
|
+
const extension = path4.extname(requestedName);
|
|
1706
|
+
const base = path4.basename(requestedName, extension);
|
|
1707
|
+
let candidate = requestedName;
|
|
1708
|
+
let suffix = 1;
|
|
1709
|
+
while (true) {
|
|
1710
|
+
try {
|
|
1711
|
+
await fs3.access(path4.join(assetsPath, candidate));
|
|
1712
|
+
candidate = `${base}-${suffix}${extension}`;
|
|
1713
|
+
suffix++;
|
|
1714
|
+
} catch {
|
|
1715
|
+
break;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
const absolutePath = path4.join(assetsPath, candidate);
|
|
1719
|
+
assertPathInside(assetsPath, absolutePath);
|
|
1720
|
+
await writeFilesAtomically([{ path: absolutePath, data: input.data }]);
|
|
1721
|
+
const value = {
|
|
1722
|
+
absolutePath,
|
|
1723
|
+
markdownPath: `./assets/${candidate}`
|
|
1724
|
+
};
|
|
1725
|
+
const refreshed = await this.inspect();
|
|
1726
|
+
return {
|
|
1727
|
+
value,
|
|
1728
|
+
changedFiles: [{ path: absolutePath, kind: "created" }],
|
|
1729
|
+
snapshotRevision: refreshed.revision
|
|
1730
|
+
};
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
};
|
|
1734
|
+
|
|
1735
|
+
// workspace/index.ts
|
|
1736
|
+
function createWorkspace(options) {
|
|
1737
|
+
return new Workspace(options);
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
export {
|
|
1741
|
+
getNewNoteReadmeBody,
|
|
1742
|
+
generateNoteTitle,
|
|
1743
|
+
WorkspaceError,
|
|
1744
|
+
parseTocCompletedNotes,
|
|
1745
|
+
createWorkspace
|
|
1746
|
+
};
|