@tnotesjs/core 0.7.0 → 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.
Files changed (55) hide show
  1. package/commands/BaseCommand.ts +58 -0
  2. package/commands/build/BuildCommand.ts +25 -0
  3. package/commands/build/PreviewCommand.ts +29 -0
  4. package/commands/build/index.ts +8 -0
  5. package/commands/dev/DevCommand.ts +75 -0
  6. package/commands/dev/index.ts +7 -0
  7. package/commands/git/PullCommand.ts +25 -0
  8. package/commands/git/PushCommand.ts +64 -0
  9. package/commands/git/index.ts +8 -0
  10. package/commands/index.ts +11 -0
  11. package/commands/init-sub-repo/InitSubRepoCommand.ts +206 -0
  12. package/commands/init-sub-repo/index.ts +1 -0
  13. package/commands/misc/HelpCommand.ts +104 -0
  14. package/commands/misc/index.ts +7 -0
  15. package/commands/models.ts +87 -0
  16. package/commands/note/CreateNoteCommand.ts +160 -0
  17. package/commands/note/RenameNoteCommand.ts +147 -0
  18. package/commands/note/UpdateNoteConfigCommand.ts +78 -0
  19. package/commands/note/index.ts +9 -0
  20. package/commands/registry.ts +47 -0
  21. package/commands/update/UpdateCommand.ts +219 -0
  22. package/commands/update/index.ts +7 -0
  23. package/commands/update-completed-count/UpdateCompletedCountCommand.ts +208 -0
  24. package/commands/update-completed-count/index.ts +5 -0
  25. package/dist/markdown/index.cjs +6 -9
  26. package/dist/markdown/index.js +6 -9
  27. package/dist/vitepress/config/index.cjs +214 -58
  28. package/dist/vitepress/config/index.js +208 -52
  29. package/markdown/components.ts +86 -0
  30. package/markdown/index.ts +17 -0
  31. package/markdown/noteFormatter.test.ts +44 -0
  32. package/markdown/noteFormatter.ts +237 -0
  33. package/package.json +7 -3
  34. package/vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue +9 -18
  35. package/vitepress/components/EnWordList/EnWordList.vue +16 -662
  36. package/vitepress/components/Footprints/Footprints.vue +15 -537
  37. package/vitepress/components/Mermaid/Mermaid.vue +13 -588
  38. package/vitepress/components/MindmapPreview/MindmapPreview.vue +12 -434
  39. package/vitepress/components/MindmapPreview/markdown.ts +1 -1
  40. package/vitepress/components/NotesTable/NotesTable.vue +11 -130
  41. package/vitepress/configs/markdown.config.ts +170 -26
  42. package/vitepress/theme/index.ts +9 -13
  43. package/vitepress/theme/styles/base.scss +15 -0
  44. package/workspace/atomic.ts +113 -0
  45. package/workspace/errors.ts +27 -0
  46. package/workspace/index.ts +40 -0
  47. package/workspace/mutationQueue.ts +28 -0
  48. package/workspace/paths.ts +64 -0
  49. package/workspace/reconcile.test.ts +300 -0
  50. package/workspace/reconcile.ts +95 -0
  51. package/workspace/scanner.ts +292 -0
  52. package/workspace/types.ts +224 -0
  53. package/workspace/workspace.test.ts +333 -0
  54. package/workspace/workspace.ts +1020 -0
  55. package/vitepress/components/EnWordList/RightClickMenu.vue +0 -93
@@ -17,8 +17,8 @@ import {
17
17
  } from "../../chunk-GWG75A5M.js";
18
18
 
19
19
  // vitepress/config/index.ts
20
- import fs3 from "fs";
21
- import path5 from "path";
20
+ import fs2 from "fs";
21
+ import path4 from "path";
22
22
  import { defineConfig } from "vitepress";
23
23
 
24
24
  // vitepress/config/dependencyOptimization.ts
@@ -63,11 +63,9 @@ function getHeadConfig(config, githubPageUrl) {
63
63
  }
64
64
 
65
65
  // vitepress/configs/markdown.config.ts
66
- import fs from "fs";
67
66
  import markdownItContainer from "markdown-it-container";
68
67
  import mila from "markdown-it-link-attributes";
69
68
  import markdownItTaskLists from "markdown-it-task-lists";
70
- import path from "path";
71
69
 
72
70
  // vitepress/components/MindmapPreview/markdown.ts
73
71
  function cleanHeadingText(value) {
@@ -90,19 +88,6 @@ function parseMindmapFence(openLine) {
90
88
  }
91
89
  return options;
92
90
  }
93
- function parseMindmapReference(line) {
94
- const match = line.trim().match(/^<<<\s+(.+?)\s*$/);
95
- if (!match) return null;
96
- let rest = match[1].trim();
97
- let title;
98
- const titleMatch = rest.match(/\s+\[([^\]]+)\]\s*$/);
99
- if (titleMatch) {
100
- title = cleanHeadingText(titleMatch[1]) || void 0;
101
- rest = rest.slice(0, titleMatch.index).trim();
102
- }
103
- const path6 = rest.replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/, "$1$2").trim();
104
- return path6 ? { path: path6, title } : null;
105
- }
106
91
  function normalizeMindmapMarkdown(source, options = {}) {
107
92
  const lines = source.replace(/\r\n?/g, "\n").split("\n");
108
93
  let existingTitle = "";
@@ -125,6 +110,79 @@ ${body.join("\n")}
125
110
  `;
126
111
  }
127
112
 
113
+ // node_modules/.pnpm/@tnotesjs+ui@0.1.1_vue@3.5.31_typescript@5.9.3_/node_modules/@tnotesjs/ui/src/components/Footprints/parse.ts
114
+ function parseFootprintsDatetime(meta) {
115
+ const m = meta.trim().match(
116
+ /^(\d{4})-(\d{1,2})-(\d{1,2})(?:\s+(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?$/
117
+ );
118
+ if (!m) return [];
119
+ const parts = [Number(m[1]), Number(m[2]), Number(m[3])];
120
+ if (m[4] !== void 0) {
121
+ parts.push(Number(m[4]), Number(m[5]));
122
+ if (m[6] !== void 0) parts.push(Number(m[6]));
123
+ }
124
+ return parts;
125
+ }
126
+ function parseFootprintsSource(source) {
127
+ const normalized = source.replace(/\r\n?/g, "\n");
128
+ const lines = normalized.split("\n");
129
+ const open = lines[0]?.match(/^ {0,3}:{3,}\s*footprints(?:\s+(.*))?$/i);
130
+ const times = parseFootprintsDatetime((open?.[1] ?? "").trim());
131
+ let end = lines.length - 1;
132
+ while (end > 0 && !/^ {0,3}:{3,}\s*$/.test(lines[end])) end -= 1;
133
+ const bodyLines = lines.slice(1, end);
134
+ const paragraphs = [];
135
+ const images = [];
136
+ let otherInfo = "";
137
+ let buf = [];
138
+ let inOther = false;
139
+ const flush = () => {
140
+ const text = buf.join("\n").trim();
141
+ buf = [];
142
+ if (!text) return;
143
+ if (inOther) {
144
+ otherInfo = otherInfo ? `${otherInfo}
145
+ ${text}` : text;
146
+ return;
147
+ }
148
+ const imgOnly = text.match(/^!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)$/);
149
+ if (imgOnly) {
150
+ images.push(imgOnly[2]);
151
+ return;
152
+ }
153
+ const parts = text.split("\n");
154
+ let allImg = true;
155
+ const blockImgs = [];
156
+ for (const line of parts) {
157
+ const m = line.trim().match(/^!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)$/);
158
+ if (m) blockImgs.push(m[2]);
159
+ else if (line.trim()) {
160
+ allImg = false;
161
+ break;
162
+ }
163
+ }
164
+ if (allImg && blockImgs.length) {
165
+ images.push(...blockImgs);
166
+ return;
167
+ }
168
+ paragraphs.push(text);
169
+ };
170
+ for (const line of bodyLines) {
171
+ if (line.trim() === "---") {
172
+ flush();
173
+ inOther = true;
174
+ continue;
175
+ }
176
+ if (line.trim() === "") {
177
+ flush();
178
+ continue;
179
+ }
180
+ buf.push(line);
181
+ }
182
+ flush();
183
+ return { times, paragraphs, images, otherInfo };
184
+ }
185
+
128
186
  // vitepress/configs/markdown.config.ts
129
187
  function esc(s = "") {
130
188
  return s.replace(
@@ -142,11 +200,14 @@ var simpleMermaidMarkdown = (md) => {
142
200
  const fence = md.renderer.rules.fence ? md.renderer.rules.fence.bind(md.renderer.rules) : () => "";
143
201
  md.renderer.rules.fence = (tokens, index, options, env, slf) => {
144
202
  const token = tokens[index];
145
- if (token.info.trim() === "mermaid") {
203
+ const parts = token.info.trim().split(/\s+/).filter(Boolean);
204
+ if (parts[0] === "mermaid") {
146
205
  try {
206
+ const centered = parts.slice(1).some((part) => part.toLowerCase() === "center");
147
207
  const key = `mermaid-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
148
208
  const content = token.content;
149
- return `<Mermaid id="${key}" graph="${encodeURIComponent(content)}" />`;
209
+ const centerAttr = centered ? ' :center="true"' : "";
210
+ return `<Mermaid id="${key}" graph="${encodeURIComponent(content)}"${centerAttr} />`;
150
211
  } catch (err) {
151
212
  return `<pre>${err}</pre>`;
152
213
  }
@@ -164,28 +225,14 @@ function configureMindmapFence(md) {
164
225
  const info = token.info.trim();
165
226
  const fenceOptions = parseMindmapFence(info);
166
227
  if (!fenceOptions) return fence(tokens, index, options, env, slf);
167
- let content = token.content;
168
- const firstNonEmptyLine = content.split("\n").find((line) => line.trim()) ?? "";
169
- const reference = parseMindmapReference(firstNonEmptyLine);
170
- if (reference) {
171
- const possibleRel = env?.relativePath || env?.path || env?.filePath || env?.file || "";
172
- const refFullPath = path.isAbsolute(reference.path) ? reference.path : path.resolve(process.cwd(), possibleRel ? path.dirname(possibleRel) : "", reference.path);
173
- try {
174
- content = fs.readFileSync(refFullPath, "utf8");
175
- } catch (error) {
176
- const message = error instanceof Error ? error.message : String(error);
177
- content = `- Failed to load referenced file: ${reference.path}
178
- - Error: ${message}`;
179
- }
180
- }
181
- content = normalizeMindmapMarkdown(content, {
182
- title: fenceOptions.title || reference?.title
228
+ const content = normalizeMindmapMarkdown(token.content, {
229
+ title: fenceOptions.title
183
230
  });
184
231
  const props = [
185
232
  `content="${encodeURIComponent(content.trim())}"`,
186
233
  fenceOptions.initialExpandLevel === void 0 ? "" : `:initialExpandLevel="${fenceOptions.initialExpandLevel}"`
187
234
  ].filter(Boolean).join(" ");
188
- return `<MindmapPreview ${props}></MindmapPreview>
235
+ return `<Mindmap ${props}></Mindmap>
189
236
  `;
190
237
  };
191
238
  }
@@ -247,6 +294,114 @@ function configureSwiperContainer(md) {
247
294
  }
248
295
  });
249
296
  }
297
+ function escapeHtmlText(s) {
298
+ return s.replace(
299
+ /[&<>"']/g,
300
+ (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[ch]
301
+ );
302
+ }
303
+ function extractFootprintsPayloadFromTokens(tokens, idx) {
304
+ const meta = String(tokens[idx].info || "").trim().replace(/^footprints\s*/i, "");
305
+ const times = parseFootprintsDatetime(meta);
306
+ const paragraphs = [];
307
+ const images = [];
308
+ let otherInfo = "";
309
+ let inOther = false;
310
+ for (let i = idx + 1; i < tokens.length; i++) {
311
+ const t = tokens[i];
312
+ if (t.type === "container_footprints_close") break;
313
+ if (t.type !== "inline") continue;
314
+ const childImgs = [];
315
+ if (Array.isArray(t.children)) {
316
+ for (const c of t.children) {
317
+ if (c.type === "image") {
318
+ const src = c.attrGet?.("src") || c.attrs?.find((a) => a[0] === "src")?.[1];
319
+ if (src) childImgs.push(src);
320
+ }
321
+ }
322
+ }
323
+ const content = String(t.content || "").trim();
324
+ if (content === "---") {
325
+ inOther = true;
326
+ continue;
327
+ }
328
+ if (childImgs.length) {
329
+ const withoutImgs = content.replace(/!\[[^\]]*\]\([^)]+\)/g, "").trim();
330
+ if (!withoutImgs) {
331
+ images.push(...childImgs);
332
+ continue;
333
+ }
334
+ }
335
+ const imgOnly = content.match(/^!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)$/);
336
+ if (imgOnly) {
337
+ images.push(imgOnly[2]);
338
+ continue;
339
+ }
340
+ if (!content) continue;
341
+ if (inOther) otherInfo = otherInfo ? `${otherInfo}
342
+ ${content}` : content;
343
+ else paragraphs.push(content);
344
+ }
345
+ return { times, paragraphs, images, otherInfo };
346
+ }
347
+ function configureFootprintsContainer(md) {
348
+ md.use(markdownItContainer, "footprints", {
349
+ validate: (params) => /^footprints(\s|$)/i.test(params.trim()),
350
+ render: (tokens, idx, _opts, env) => {
351
+ if (tokens[idx].nesting !== 1) return "";
352
+ let payload = extractFootprintsPayloadFromTokens(tokens, idx);
353
+ const startLine = tokens[idx].map?.[0] ?? 0;
354
+ let endLine = startLine;
355
+ for (let i = idx + 1; i < tokens.length; i++) {
356
+ if (tokens[i].type === "container_footprints_close") {
357
+ endLine = tokens[i].map?.[0] ?? endLine;
358
+ break;
359
+ }
360
+ }
361
+ const raw = String(env?.src ?? env?.source ?? "");
362
+ if (raw && endLine > startLine) {
363
+ const slice = raw.split(/\r?\n/).slice(startLine, endLine + 1).join("\n");
364
+ const fromSource = parseFootprintsSource(
365
+ slice.includes(":::") ? slice : `::: ${tokens[idx].info}
366
+ ${slice}
367
+ :::`
368
+ );
369
+ if (!payload.paragraphs.length && fromSource.paragraphs.length) {
370
+ payload = { ...payload, paragraphs: fromSource.paragraphs };
371
+ }
372
+ if (!payload.images.length && fromSource.images.length) {
373
+ payload = { ...payload, images: fromSource.images };
374
+ }
375
+ if (!payload.otherInfo && fromSource.otherInfo) {
376
+ payload = { ...payload, otherInfo: fromSource.otherInfo };
377
+ }
378
+ if (!payload.times.length && fromSource.times.length) {
379
+ payload = { ...payload, times: fromSource.times };
380
+ }
381
+ }
382
+ for (let i = idx + 1; i < tokens.length; i++) {
383
+ if (tokens[i].type === "container_footprints_close") break;
384
+ tokens[i].hidden = true;
385
+ tokens[i].content = "";
386
+ if (Array.isArray(tokens[i].children)) {
387
+ for (const child of tokens[i].children) {
388
+ child.hidden = true;
389
+ child.content = "";
390
+ }
391
+ tokens[i].children = [];
392
+ }
393
+ }
394
+ const enc = (value) => encodeURIComponent(JSON.stringify(value)).replace(/'/g, "%27");
395
+ const bindExpr = (value) => "JSON.parse(decodeURIComponent('" + enc(value) + "'))";
396
+ const imageSlot = payload.images.map((src, i) => {
397
+ return '<img src="' + escapeHtmlText(src) + '" @click="openModal(' + String(i) + ')" />';
398
+ }).join("\n");
399
+ const openTag = '<Footprints :times="' + bindExpr(payload.times) + '" :paragraphs="' + bindExpr(payload.paragraphs) + '" :other-info="' + bindExpr(payload.otherInfo) + '">';
400
+ if (!payload.images.length) return openTag + "</Footprints>\n";
401
+ return openTag + '\n<template #image-list="{ openModal }">\n' + imageSlot + "\n</template>\n</Footprints>\n";
402
+ }
403
+ });
404
+ }
250
405
  function getMarkdownConfig() {
251
406
  const markdown = {
252
407
  lineNumbers: true,
@@ -266,6 +421,7 @@ function getMarkdownConfig() {
266
421
  }
267
422
  });
268
423
  configureSwiperContainer(md);
424
+ configureFootprintsContainer(md);
269
425
  },
270
426
  anchor: {
271
427
  slugify: generateAnchor
@@ -753,19 +909,19 @@ function getNoteByConfigIdPlugin() {
753
909
  }
754
910
 
755
911
  // vitepress/plugins/localSearchReindexPlugin.ts
756
- import path4 from "path";
912
+ import path3 from "path";
757
913
 
758
914
  // vitepress/plugins/localSearchIndexBuilder.ts
759
915
  import MiniSearch from "minisearch";
760
- import fs2 from "fs/promises";
761
- import path3 from "path";
916
+ import fs from "fs/promises";
917
+ import path2 from "path";
762
918
  import {
763
919
  createMarkdownRenderer,
764
920
  resolvePages
765
921
  } from "vitepress";
766
922
 
767
923
  // vitepress/plugins/localSearchReindexLogic.ts
768
- import path2 from "path";
924
+ import path from "path";
769
925
  function slash(value) {
770
926
  return value.replace(/\\/g, "/");
771
927
  }
@@ -781,13 +937,13 @@ function isNoteReadmePath(filePath, ignoreDirNames = []) {
781
937
  function getDocIdFromFile(input) {
782
938
  const pathApi = /^[a-zA-Z]:[\\/]/.test(
783
939
  input.absoluteOrRelativeFile || input.srcDir
784
- ) ? path2.win32 : path2;
940
+ ) ? path.win32 : path;
785
941
  const absoluteFile = pathApi.isAbsolute(input.absoluteOrRelativeFile) ? input.absoluteOrRelativeFile : pathApi.join(input.srcDir, input.absoluteOrRelativeFile);
786
942
  let relFile = slash(pathApi.relative(input.srcDir, absoluteFile));
787
943
  if (input.rewrites?.[relFile]) {
788
944
  relFile = input.rewrites[relFile];
789
945
  }
790
- let id = slash(path2.posix.join(input.base, relFile));
946
+ let id = slash(path.posix.join(input.base, relFile));
791
947
  id = id.replace(/(^|\/)index\.md$/, "$1");
792
948
  id = id.replace(/\.md$/, input.cleanUrls ? "" : ".html");
793
949
  return id;
@@ -852,9 +1008,9 @@ async function createLocalSearchMarkdownRenderer(siteConfig) {
852
1008
  );
853
1009
  }
854
1010
  async function renderPageHtml(siteConfig, md, absoluteFile) {
855
- if (!await fs2.stat(absoluteFile).catch(() => null)) return "";
1011
+ if (!await fs.stat(absoluteFile).catch(() => null)) return "";
856
1012
  const srcDir = siteConfig.srcDir;
857
- const relativePath = slash2(path3.relative(srcDir, absoluteFile));
1013
+ const relativePath = slash2(path2.relative(srcDir, absoluteFile));
858
1014
  const env = {
859
1015
  path: absoluteFile,
860
1016
  relativePath,
@@ -862,7 +1018,7 @@ async function renderPageHtml(siteConfig, md, absoluteFile) {
862
1018
  frontmatter: {}
863
1019
  };
864
1020
  const options = siteConfig.site.themeConfig?.search?.options ?? {};
865
- const mdRaw = await fs2.readFile(absoluteFile, "utf-8");
1021
+ const mdRaw = await fs.readFile(absoluteFile, "utf-8");
866
1022
  if (options._render) {
867
1023
  return await options._render(mdRaw, env, md);
868
1024
  }
@@ -870,7 +1026,7 @@ async function renderPageHtml(siteConfig, md, absoluteFile) {
870
1026
  return env.frontmatter?.search === false ? "" : html;
871
1027
  }
872
1028
  async function indexPage(siteConfig, md, page, index) {
873
- const absoluteFile = path3.join(siteConfig.srcDir, page);
1029
+ const absoluteFile = path2.join(siteConfig.srcDir, page);
874
1030
  const fileId = getDocIdFromFile({
875
1031
  srcDir: siteConfig.srcDir,
876
1032
  base: siteConfig.site.base,
@@ -1160,7 +1316,7 @@ function localSearchReindexPlugin() {
1160
1316
  const onWatcherEvent = (absoluteFile, event) => {
1161
1317
  if (!isNoteReadmePath(absoluteFile, ignoreDirNames)) return;
1162
1318
  scheduleNoteSearchReindex(
1163
- `watcher:${event}:${path4.basename(absoluteFile)}`
1319
+ `watcher:${event}:${path3.basename(absoluteFile)}`
1164
1320
  );
1165
1321
  };
1166
1322
  server.watcher.on("add", (file) => onWatcherEvent(file, "add"));
@@ -1314,14 +1470,14 @@ function isNoteInSubtree(snapshot, lineIndex, noteIndex) {
1314
1470
  }
1315
1471
  function folderPathAtLine(snapshot, lineIndex) {
1316
1472
  const result = [];
1317
- const walk = (nodes, path6) => {
1473
+ const walk = (nodes, path5) => {
1318
1474
  for (const node of nodes) {
1319
1475
  if (node.kind === "folder") {
1320
1476
  if (node.tocLineIndex === lineIndex) {
1321
- result.push(...[...path6, node.title]);
1477
+ result.push(...[...path5, node.title]);
1322
1478
  return true;
1323
1479
  }
1324
- if (walk(node.children, [...path6, node.title])) return true;
1480
+ if (walk(node.children, [...path5, node.title])) return true;
1325
1481
  }
1326
1482
  }
1327
1483
  return false;
@@ -1712,9 +1868,9 @@ function defineNotesConfig(overrides = {}) {
1712
1868
  },
1713
1869
  transformPageData(pageData, ctx) {
1714
1870
  if (/^notes\/\d{4}/.test(pageData.relativePath)) {
1715
- const fullPath = path5.resolve(rootPath, pageData.relativePath);
1871
+ const fullPath = path4.resolve(rootPath, pageData.relativePath);
1716
1872
  try {
1717
- const raw = fs3.readFileSync(fullPath, "utf-8");
1873
+ const raw = fs2.readFileSync(fullPath, "utf-8");
1718
1874
  pageData.frontmatter.rawContent = Buffer.from(raw, "utf-8").toString(
1719
1875
  "base64"
1720
1876
  );
@@ -0,0 +1,86 @@
1
+ export type TNotesComponentKind =
2
+ | 'block-component'
3
+ | 'inline-component'
4
+ | 'container'
5
+ | 'fenced-language'
6
+
7
+ export interface TNotesComponentDescriptor {
8
+ name: string
9
+ aliases?: string[]
10
+ kind: TNotesComponentKind
11
+ editable: 'visual' | 'source-only' | 'placeholder'
12
+ }
13
+
14
+ /**
15
+ * Runtime-neutral registry shared by Desk and other TNotes clients. It does not
16
+ * import Vue or VitePress and is therefore safe in Node/Electron workers.
17
+ */
18
+ export const TNOTES_COMPONENTS: readonly TNotesComponentDescriptor[] = [
19
+ {
20
+ name: 'BilibiliVideo',
21
+ kind: 'block-component',
22
+ editable: 'visual',
23
+ },
24
+ {
25
+ name: 'WordList',
26
+ kind: 'block-component',
27
+ editable: 'visual',
28
+ },
29
+ {
30
+ name: 'Footprints',
31
+ kind: 'block-component',
32
+ editable: 'visual',
33
+ },
34
+ {
35
+ name: 'NotesTable',
36
+ kind: 'block-component',
37
+ editable: 'visual',
38
+ },
39
+ {
40
+ name: 'Tooltip',
41
+ kind: 'inline-component',
42
+ editable: 'visual',
43
+ },
44
+ {
45
+ name: 'Mindmap',
46
+ aliases: ['MindmapPreview'],
47
+ kind: 'block-component',
48
+ editable: 'visual',
49
+ },
50
+ {
51
+ name: 'Mermaid',
52
+ kind: 'block-component',
53
+ editable: 'visual',
54
+ },
55
+ {
56
+ name: 'Discussions',
57
+ kind: 'block-component',
58
+ editable: 'placeholder',
59
+ },
60
+ {
61
+ name: 'SidebarCard',
62
+ kind: 'block-component',
63
+ editable: 'placeholder',
64
+ },
65
+ { name: 'swiper', kind: 'container', editable: 'visual' },
66
+ { name: 'code-group', kind: 'container', editable: 'visual' },
67
+ { name: 'footprints', kind: 'container', editable: 'visual' },
68
+ { name: 'details', kind: 'container', editable: 'visual' },
69
+ { name: 'info', kind: 'container', editable: 'visual' },
70
+ { name: 'tip', kind: 'container', editable: 'visual' },
71
+ { name: 'warning', kind: 'container', editable: 'visual' },
72
+ { name: 'danger', kind: 'container', editable: 'visual' },
73
+ { name: 'mermaid', kind: 'fenced-language', editable: 'visual' },
74
+ { name: 'mindmap', kind: 'fenced-language', editable: 'visual' },
75
+ ] as const
76
+
77
+ export function findTNotesComponent(
78
+ name: string,
79
+ ): TNotesComponentDescriptor | undefined {
80
+ const normalized = name.toLowerCase()
81
+ return TNOTES_COMPONENTS.find(
82
+ (component) =>
83
+ component.name.toLowerCase() === normalized ||
84
+ component.aliases?.some((alias) => alias.toLowerCase() === normalized),
85
+ )
86
+ }
@@ -0,0 +1,17 @@
1
+ export {
2
+ NOTE_TOC_END_TAG,
3
+ NOTE_TOC_START_TAG,
4
+ formatTNotesNote,
5
+ } from './noteFormatter'
6
+ export type {
7
+ FormatTNotesNoteInput,
8
+ FormatTNotesNoteResult,
9
+ } from './noteFormatter'
10
+ export {
11
+ TNOTES_COMPONENTS,
12
+ findTNotesComponent,
13
+ } from './components'
14
+ export type {
15
+ TNotesComponentDescriptor,
16
+ TNotesComponentKind,
17
+ } from './components'
@@ -0,0 +1,44 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import {
4
+ findTNotesComponent,
5
+ formatTNotesNote,
6
+ TNOTES_COMPONENTS,
7
+ } from './index'
8
+
9
+ describe('TNotes Markdown contract', () => {
10
+ it('exposes runtime-neutral component descriptors', () => {
11
+ expect(findTNotesComponent('NotesTable')?.name).toBe('NotesTable')
12
+ expect(findTNotesComponent('WordList')?.name).toBe('WordList')
13
+ expect(findTNotesComponent('BilibiliVideo')?.name).toBe('BilibiliVideo')
14
+ expect(findTNotesComponent('N')).toBeUndefined()
15
+ expect(findTNotesComponent('SWIPER')?.kind).toBe('container')
16
+ expect(findTNotesComponent('Mermaid')?.editable).toBe('visual')
17
+ expect(TNOTES_COMPONENTS.some((item) => item.name === 'markmap')).toBe(false)
18
+ expect(TNOTES_COMPONENTS.some((item) => item.name === 'mindmap')).toBe(true)
19
+ })
20
+
21
+ it('restores missing generated markers without interpreting fenced headings', async () => {
22
+ const result = await formatTNotesNote({
23
+ content: '## Heading\n\n~~~md\n### source\n~~~\n',
24
+ noteIndex: '0042',
25
+ title: 'Answer',
26
+ repoOwner: 'tnotesjs',
27
+ repoName: 'TNotes.fixture',
28
+ noteConfig: {
29
+ id: 'answer',
30
+ bilibili: [],
31
+ tnotes: [],
32
+ yuque: [],
33
+ done: false,
34
+ enableDiscussions: false,
35
+ },
36
+ prettier: false,
37
+ })
38
+
39
+ expect(result.content).toContain('<!-- region:toc -->')
40
+ expect(result.content).toContain('<!-- endregion:toc -->')
41
+ expect(result.generatedToc).toEqual(['- [1. Heading](#1-heading)'])
42
+ expect(result.content).toContain('~~~md\n### source\n~~~')
43
+ })
44
+ })