@jant/core 0.7.0 → 0.7.1

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 (158) hide show
  1. package/bin/commands/export.js +3 -1
  2. package/bin/commands/import-site.js +689 -216
  3. package/bin/commands/setup.js +136 -0
  4. package/bin/commands/site/export.js +71 -34
  5. package/bin/commands/site/pull-media.js +2 -6
  6. package/bin/commands/site/snapshot/export.js +18 -60
  7. package/bin/commands/site/snapshot/import.js +22 -23
  8. package/bin/lib/d1-query.js +87 -2
  9. package/bin/lib/hugo-markdown.js +4 -0
  10. package/bin/lib/site-pull-media.js +21 -28
  11. package/bin/lib/site-selection.js +10 -1
  12. package/bin/lib/site-snapshot.js +338 -3
  13. package/bin/lib/sql-export.js +68 -5
  14. package/bin/lib/wrangler-cli.js +9 -0
  15. package/bin/lib/zip-archive.js +187 -0
  16. package/dist/{app-K_Aa1MMn.js → app-ZcaI1kPN.js} +732 -217
  17. package/dist/client/.vite/manifest.json +20 -20
  18. package/dist/client/_assets/chunks/{create-editor-CD3FhrOB.js → create-editor-B-m7X7S5.js} +50 -46
  19. package/dist/client/_assets/chunks/{sortable-list-CgaL2jCs.js → sortable-list-BJyd-LXE.js} +1 -1
  20. package/dist/client/_assets/chunks/{unsafe-svg-0QCkP0vZ.js → unsafe-svg-BkscJx69.js} +2 -2
  21. package/dist/client/_assets/{client-GiYENVw8.css → client-DAhoqPdr.css} +1 -1
  22. package/dist/client/_assets/{client-auth-k8gJ6QJj.js → client-auth-CAMfTrKW.js} +1 -1
  23. package/dist/client/_assets/{client-compose-B_kDtWkW.js → client-compose-D6rExKVK.js} +1 -1
  24. package/dist/client/_assets/{client-VnFxJN7G.js → client-knSEyUJO.js} +1 -1
  25. package/dist/client/_assets/{client-manage-M90aSTOk.js → client-manage-sVOkqVbH.js} +1 -1
  26. package/dist/client/_assets/{client-settings-DfYs9n1F.js → client-settings-CGR_nZ78.js} +7 -7
  27. package/dist/{github-sync-BPAvT999.js → github-sync-Gw4orAHk.js} +799 -110
  28. package/dist/index.js +2 -2
  29. package/dist/node.js +9 -5
  30. package/package.json +13 -3
  31. package/src/__tests__/dev-scripts.test.ts +203 -0
  32. package/src/__tests__/export-collection-order.test.ts +276 -0
  33. package/src/__tests__/export-feed-ids.test.ts +184 -0
  34. package/src/__tests__/export-feed-order.test.ts +294 -0
  35. package/src/__tests__/export-hugo-build.test.ts +130 -0
  36. package/src/__tests__/export-import-roundtrip.test.ts +94 -0
  37. package/src/__tests__/export-service.test.ts +611 -28
  38. package/src/__tests__/export-smart-collection.test.ts +329 -0
  39. package/src/__tests__/helpers/hugo-site.ts +146 -0
  40. package/src/__tests__/import-site-command.test.ts +487 -1
  41. package/src/__tests__/mise-config.test.ts +75 -4
  42. package/src/__tests__/node-dev-tasks.test.ts +264 -0
  43. package/src/__tests__/site-export-canonical-import.test.ts +149 -0
  44. package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
  45. package/src/__tests__/snapshot-settings.test.ts +97 -0
  46. package/src/__tests__/snapshot-tables.test.ts +219 -0
  47. package/src/__tests__/sql-export.test.ts +173 -0
  48. package/src/__tests__/zip-archive.test.ts +106 -0
  49. package/src/app.tsx +15 -6
  50. package/src/client/components/__tests__/jant-settings-avatar.test.ts +1 -1
  51. package/src/client/components/__tests__/jant-settings-general.test.ts +21 -1
  52. package/src/client/components/jant-repo-picker-types.ts +6 -1
  53. package/src/client/components/jant-repo-picker.ts +4 -7
  54. package/src/client/components/jant-settings-general.ts +17 -12
  55. package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
  56. package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
  57. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
  58. package/src/client/tiptap/extensions.ts +0 -3
  59. package/src/client/tiptap/structural-keymap.ts +158 -47
  60. package/src/db/__tests__/d1-query.test.ts +56 -1
  61. package/src/i18n/locales/settings/en.po +8 -8
  62. package/src/i18n/locales/settings/en.ts +1 -1
  63. package/src/i18n/locales/settings/zh-Hans.po +8 -8
  64. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  65. package/src/i18n/locales/settings/zh-Hant.po +8 -8
  66. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  67. package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
  68. package/src/lib/__tests__/image.test.ts +27 -1
  69. package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
  70. package/src/lib/__tests__/markdown.test.ts +10 -0
  71. package/src/lib/__tests__/resolve-config.test.ts +67 -0
  72. package/src/lib/__tests__/schemas.test.ts +27 -1
  73. package/src/lib/__tests__/timeline.test.ts +87 -0
  74. package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
  75. package/src/lib/discover.ts +3 -1
  76. package/src/lib/github-sync-repo-name.ts +45 -0
  77. package/src/lib/hugo-markdown.ts +46 -0
  78. package/src/lib/image.ts +17 -4
  79. package/src/lib/markdown-manager.ts +392 -2
  80. package/src/lib/post-body-html.ts +11 -4
  81. package/src/lib/resolve-config.ts +58 -1
  82. package/src/lib/schemas.ts +50 -5
  83. package/src/lib/thread-fold.ts +3 -3
  84. package/src/lib/timeline.ts +1 -1
  85. package/src/lib/tiptap-to-markdown.ts +13 -7
  86. package/src/lib/url.ts +20 -0
  87. package/src/lib/view.ts +2 -2
  88. package/src/node/__tests__/cli-setup.test.ts +163 -0
  89. package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
  90. package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
  91. package/src/node/__tests__/runtime.test.ts +38 -0
  92. package/src/node/index.ts +2 -0
  93. package/src/node/request-handler.ts +3 -1
  94. package/src/routes/api/__tests__/posts.test.ts +24 -0
  95. package/src/routes/api/__tests__/upload.test.ts +34 -0
  96. package/src/routes/api/export.ts +3 -3
  97. package/src/routes/api/internal/sites.ts +0 -1
  98. package/src/routes/api/posts.ts +2 -0
  99. package/src/routes/api/public/posts.ts +21 -1
  100. package/src/routes/api/upload.ts +10 -3
  101. package/src/routes/compose.tsx +4 -0
  102. package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
  103. package/src/routes/dash/settings.tsx +212 -70
  104. package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
  105. package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
  106. package/src/routes/pages/archive.tsx +17 -11
  107. package/src/routes/pages/featured.tsx +11 -5
  108. package/src/routes/pages/page.tsx +53 -31
  109. package/src/routes/pages/search.tsx +4 -2
  110. package/src/runtime/__tests__/readiness.test.ts +23 -0
  111. package/src/runtime/node.ts +49 -0
  112. package/src/runtime/readiness.ts +15 -0
  113. package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
  114. package/src/services/__tests__/custom-url.test.ts +34 -0
  115. package/src/services/__tests__/github-app-installations.test.ts +153 -0
  116. package/src/services/__tests__/github-sync-push.test.ts +46 -0
  117. package/src/services/__tests__/media.test.ts +31 -0
  118. package/src/services/__tests__/path.test.ts +56 -0
  119. package/src/services/__tests__/post-timeline.test.ts +127 -0
  120. package/src/services/__tests__/post.test.ts +74 -0
  121. package/src/services/bootstrap.ts +263 -44
  122. package/src/services/custom-url.ts +2 -2
  123. package/src/services/export-theme/layouts/_default/alias.html +27 -1
  124. package/src/services/export-theme/layouts/_default/list.html +2 -63
  125. package/src/services/export-theme/layouts/_default/rss.xml +27 -38
  126. package/src/services/export-theme/layouts/collections/list.html +3 -3
  127. package/src/services/export-theme/layouts/featured/list.html +1 -4
  128. package/src/services/export-theme/layouts/index.html +40 -26
  129. package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
  130. package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
  131. package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
  132. package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
  133. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  134. package/src/services/export-theme/layouts/partials/head.html +1 -1
  135. package/src/services/export-theme/layouts/partials/header.html +5 -3
  136. package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
  137. package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
  138. package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
  139. package/src/services/export-theme/layouts/partials/thread-preview.html +1 -1
  140. package/src/services/export-theme/layouts/post/list.html +1 -1
  141. package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
  142. package/src/services/export-theme/styles/main.css +0 -1
  143. package/src/services/export-theme/theme.toml +1 -1
  144. package/src/services/export.ts +620 -71
  145. package/src/services/github-app-installations.ts +171 -4
  146. package/src/services/github-sync.ts +69 -15
  147. package/src/services/mcp.ts +19 -1
  148. package/src/services/media.ts +8 -1
  149. package/src/services/path.ts +34 -1
  150. package/src/services/post.ts +214 -39
  151. package/src/services/site-admin.ts +3 -7
  152. package/src/services/site.ts +83 -31
  153. package/src/styles/ui.css +7 -1
  154. package/src/types/app-context.ts +16 -0
  155. package/src/types/bindings.ts +5 -0
  156. package/src/types/operations.ts +10 -0
  157. package/src/ui/dash/settings/GeneralContent.tsx +10 -8
  158. package/src/client/tiptap/exitable-marks.ts +0 -73
@@ -511,10 +511,11 @@ export class JantSettingsGeneral extends LitElement {
511
511
  *
512
512
  * The same `resolveDiscoverMode` the server runs, over the same inputs, so
513
513
  * the checkbox cannot disagree with the feed. It has to be computed here
514
- * rather than sent down resolved because two of its inputs — search
515
- * indexing and the feed switch — are controls on this page: a value resolved
516
- * on the server is correct until the first click and wrong from then until
517
- * the next page load, which is exactly the state an owner reads it in.
514
+ * rather than sent down resolved because one of its inputs, search indexing,
515
+ * is a control on this page: a value resolved on the server is correct until
516
+ * the first click and wrong from then until the next page load, which is
517
+ * exactly the state an owner reads it in. The feed switch is not — it lives
518
+ * in the Config Editor, so `feedsEnabled` holds for the life of the page.
518
519
  */
519
520
  private _effectiveDiscoverMode(overrides: { noindex?: boolean } = {}) {
520
521
  return resolveDiscoverMode({
@@ -1004,13 +1005,13 @@ export class JantSettingsGeneral extends LitElement {
1004
1005
  <span>${this.labels.discoverEnabled}</span>
1005
1006
  </label>
1006
1007
  <p class="text-sm text-muted-foreground">
1007
- ${
1008
+ ${this._renderDiscoverHelp(
1008
1009
  locked
1009
1010
  ? this.demoMode
1010
1011
  ? this.labels.discoverDemoLocked
1011
1012
  : this.labels.discoverFeedsOffLocked
1012
- : this._renderDiscoverIntro()
1013
- }
1013
+ : this.labels.discoverIntro,
1014
+ )}
1014
1015
  </p>
1015
1016
  ${
1016
1017
  heldBackBySearch
@@ -1026,14 +1027,18 @@ export class JantSettingsGeneral extends LitElement {
1026
1027
  }
1027
1028
 
1028
1029
  /**
1029
- * The help line, with each page it names linking to that page.
1030
+ * The help line under the checkbox, with each page it names linking to that
1031
+ * page.
1030
1032
  *
1031
- * Which words link where is `discoverIntroRuns`'s to decide, so the setup
1032
- * screen, which shows the same line, links the same words.
1033
+ * Whichever line is showing goes through here, the usual one or the notice
1034
+ * that replaces it while the box is locked, so a demo site's notice links the
1035
+ * directory's name too. Which words link where is `discoverIntroRuns`'s to
1036
+ * decide, so the setup screen, which shows the same opening, links the same
1037
+ * words.
1033
1038
  */
1034
- private _renderDiscoverIntro() {
1039
+ private _renderDiscoverHelp(line: string) {
1035
1040
  const runs = discoverIntroRuns(
1036
- this.labels.discoverIntro ?? "",
1041
+ line ?? "",
1037
1042
  {
1038
1043
  name: this.labels.discoverName ?? "",
1039
1044
  rules: this.labels.discoverRules ?? "",
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { afterEach, describe, expect, it } from "vitest";
4
4
  import { Editor } from "@tiptap/core";
5
+ import type { Node as ProseMirrorNode } from "@tiptap/pm/model";
5
6
  import { TextSelection } from "@tiptap/pm/state";
6
7
  import { createMarkdownContentExtensions } from "../../../lib/markdown-manager.js";
7
8
  import { ContinuousLists } from "../continuous-lists.js";
@@ -42,6 +43,7 @@ function paragraphPosition(
42
43
  let position: number | null = null;
43
44
 
44
45
  editor.state.doc.descendants((node, pos) => {
46
+ if (position !== null) return false;
45
47
  if (node.type.name !== "paragraph" || node.textContent !== text) return;
46
48
 
47
49
  position = pos + 1 + (edge === "end" ? node.content.size : 0);
@@ -52,6 +54,69 @@ function paragraphPosition(
52
54
  return position;
53
55
  }
54
56
 
57
+ function editorAt(
58
+ content: string,
59
+ text: string,
60
+ edge: "start" | "end",
61
+ ): Editor {
62
+ const editor = createEditor(content);
63
+ setCursor(editor, paragraphPosition(editor, text, edge));
64
+ return editor;
65
+ }
66
+
67
+ /**
68
+ * Renders lists as Markdown-like lines with the caret as `|`, so an assertion
69
+ * reads like the structure the author sees. A markerless paragraph inside an
70
+ * item aligns with the item's text; the trailing empty paragraph is omitted.
71
+ */
72
+ function outline(editor: Editor): string {
73
+ const { selection } = editor.state;
74
+ const lines: string[] = [];
75
+
76
+ const lineText = (node: ProseMirrorNode, pos: number): string => {
77
+ const start = pos + 1;
78
+ const offset = selection.from - start;
79
+ if (offset < 0 || offset > node.content.size) return node.textContent;
80
+ return `${node.textContent.slice(0, offset)}|${node.textContent.slice(offset)}`;
81
+ };
82
+
83
+ const walk = (
84
+ parent: ProseMirrorNode,
85
+ contentStart: number,
86
+ indent: string,
87
+ ) => {
88
+ parent.forEach((child, offset) => {
89
+ const pos = contentStart + offset;
90
+ if (child.isTextblock) {
91
+ lines.push(`${indent}${lineText(child, pos)}`);
92
+ return;
93
+ }
94
+
95
+ const ordered = child.type.name === "orderedList";
96
+ if (!ordered && child.type.name !== "bulletList") {
97
+ lines.push(`${indent}[${child.type.name}]`);
98
+ return;
99
+ }
100
+
101
+ let number = Number(child.attrs.start ?? 1);
102
+ child.forEach((item, itemOffset) => {
103
+ const marker = ordered ? `${number++}. ` : "- ";
104
+ const firstLine = lines.length;
105
+ walk(
106
+ item,
107
+ pos + 1 + itemOffset + 1,
108
+ indent + " ".repeat(marker.length),
109
+ );
110
+ lines[firstLine] = `${indent}${marker}${lines[firstLine]?.trimStart()}`;
111
+ });
112
+ });
113
+ };
114
+
115
+ walk(editor.state.doc, 0, "");
116
+ if (lines.at(-1) === "") lines.pop();
117
+ return lines.join("\n");
118
+ }
119
+
55
120
  function typeText(editor: Editor, text: string): void {
56
121
  for (const character of text) {
57
122
  const { from, to } = editor.state.selection;
@@ -317,112 +382,185 @@ describe("list editing", () => {
317
382
  expect(secondItem?.child(1).textContent).toBe("Third paragraph");
318
383
  });
319
384
 
320
- it.each([
321
- ["ordered", "ol"],
322
- ["bullet", "ul"],
323
- ])(
324
- "merges adjacent %s items in one Backspace, matching forward Delete",
325
- (_name, tag) => {
326
- const html = `<${tag}><li><p>A</p></li><li><p>B</p></li></${tag}>`;
327
- const backwardEditor = createEditor(html);
328
- const forwardEditor = createEditor(html);
329
-
330
- setCursor(
331
- backwardEditor,
332
- paragraphPosition(backwardEditor, "B", "start"),
385
+ describe("Backspace at the start of a line", () => {
386
+ it("removes a top-level marker first, then merges into the line above", () => {
387
+ const editor = editorAt(
388
+ "<ul><li><p>1</p><ul><li><p>a</p></li><li><p>c</p></li></ul></li><li><p>2</p></li></ul>",
389
+ "2",
390
+ "start",
333
391
  );
334
- setCursor(forwardEditor, paragraphPosition(forwardEditor, "A", "end"));
335
392
 
336
- expect(pressKey(backwardEditor, "Backspace")).toBe(true);
337
- expect(pressKey(forwardEditor, "Delete")).toBe(true);
393
+ expect(pressKey(editor, "Backspace")).toBe(true);
394
+ expect(outline(editor)).toBe(["- 1", " - a", " - c", "|2"].join("\n"));
338
395
 
339
- expect(backwardEditor.state.doc.firstChild?.toJSON()).toEqual(
340
- forwardEditor.state.doc.firstChild?.toJSON(),
396
+ expect(pressKey(editor, "Backspace")).toBe(true);
397
+ expect(outline(editor)).toBe(["- 1", " - a", " - c|2"].join("\n"));
398
+ });
399
+
400
+ it("rejoins a split ordered list once the unmarked line merges", () => {
401
+ const editor = editorAt(
402
+ "<ol><li><p>one</p></li><li><p>two</p></li><li><p>three</p></li></ol>",
403
+ "two",
404
+ "start",
341
405
  );
342
- expect(backwardEditor.state.doc.firstChild?.childCount).toBe(1);
343
- expect(backwardEditor.state.doc.firstChild?.firstChild?.textContent).toBe(
344
- "AB",
406
+
407
+ expect(pressKey(editor, "Backspace")).toBe(true);
408
+ expect(outline(editor)).toBe(["1. one", "|two", "1. three"].join("\n"));
409
+
410
+ expect(pressKey(editor, "Backspace")).toBe(true);
411
+ expect(outline(editor)).toBe(["1. one|two", "2. three"].join("\n"));
412
+ });
413
+
414
+ it("keeps a nested item inside its parent when removing its marker", () => {
415
+ const editor = editorAt(
416
+ "<ul><li><p>1</p><ul><li><p>a</p></li><li><p>b</p></li><li><p>c</p></li></ul></li></ul>",
417
+ "b",
418
+ "start",
345
419
  );
346
- },
347
- );
348
420
 
349
- it("removes a list marker without reordering existing subtrees", () => {
350
- const editor = createEditor(
351
- "<ul><li><p>A</p><ul><li><p>P</p></li></ul></li><li><p>B</p><ul><li><p>X</p></li></ul></li><li><p>C</p></li></ul>",
352
- );
353
- setCursor(editor, paragraphPosition(editor, "B", "start"));
421
+ expect(pressKey(editor, "Backspace")).toBe(true);
422
+ expect(outline(editor)).toBe(
423
+ ["- 1", " - a", " |b", " - c"].join("\n"),
424
+ );
354
425
 
355
- expect(pressKey(editor, "Backspace")).toBe(true);
426
+ expect(pressKey(editor, "Backspace")).toBe(true);
427
+ expect(outline(editor)).toBe(["- 1", " - a|b", " - c"].join("\n"));
428
+ });
356
429
 
357
- const list = editor.state.doc.firstChild;
358
- const mergedItem = list?.firstChild;
359
- expect(list?.type.name).toBe("bulletList");
360
- expect(list?.childCount).toBe(2);
361
- expect(
362
- mergedItem?.content.content.map((node) => [
363
- node.type.name,
364
- node.textContent,
365
- ]),
366
- ).toEqual([
367
- ["paragraph", "A"],
368
- ["bulletList", "P"],
369
- ["paragraph", "B"],
370
- ["bulletList", "X"],
371
- ]);
372
- expect(list?.child(1).textContent).toBe("C");
373
- });
430
+ it("moves an unmarked item's children up one level", () => {
431
+ const editor = editorAt(
432
+ "<ul><li><p>1</p><ul><li><p>a</p></li><li><p>b</p><ul><li><p>x</p></li></ul></li><li><p>c</p></li></ul></li></ul>",
433
+ "b",
434
+ "start",
435
+ );
374
436
 
375
- it("preserves the current item's subtree during a direct merge", () => {
376
- const editor = createEditor(
377
- "<ul><li><p>A</p></li><li><p>B</p><ul><li><p>Child</p></li></ul></li></ul>",
378
- );
379
- setCursor(editor, paragraphPosition(editor, "B", "start"));
437
+ expect(pressKey(editor, "Backspace")).toBe(true);
438
+ expect(outline(editor)).toBe(
439
+ ["- 1", " - a", " |b", " - x", " - c"].join("\n"),
440
+ );
441
+ });
380
442
 
381
- expect(pressKey(editor, "Backspace")).toBe(true);
443
+ it("merges a first nested item into its parent line in two presses", () => {
444
+ const editor = editorAt(
445
+ "<ul><li><p>Parent</p><ul><li><p>Child</p></li><li><p>Sibling</p></li></ul></li></ul>",
446
+ "Child",
447
+ "start",
448
+ );
382
449
 
383
- const mergedItem = editor.state.doc.firstChild?.firstChild;
384
- expect(editor.state.doc.firstChild?.childCount).toBe(1);
385
- expect(mergedItem?.firstChild?.textContent).toBe("AB");
386
- expect(mergedItem?.lastChild?.type.name).toBe("bulletList");
387
- expect(mergedItem?.lastChild?.firstChild?.textContent).toBe("Child");
388
- });
450
+ expect(pressKey(editor, "Backspace")).toBe(true);
451
+ expect(outline(editor)).toBe(
452
+ ["- Parent", " |Child", " - Sibling"].join("\n"),
453
+ );
389
454
 
390
- it("restores list items and subtrees with undo", () => {
391
- const editor = createEditor(
392
- "<ul><li><p>A</p></li><li><p>B</p><ul><li><p>Child</p></li></ul></li></ul>",
393
- );
394
- setCursor(editor, paragraphPosition(editor, "B", "start"));
395
- const before = editor.getJSON();
455
+ expect(pressKey(editor, "Backspace")).toBe(true);
456
+ expect(outline(editor)).toBe(
457
+ ["- Parent|Child", " - Sibling"].join("\n"),
458
+ );
459
+ });
396
460
 
397
- expect(pressKey(editor, "Backspace")).toBe(true);
398
- expect(editor.commands.undo()).toBe(true);
399
- expect(editor.getJSON()).toEqual(before);
400
- });
461
+ it("removes an empty nested item and returns to the end of the line above", () => {
462
+ const editor = editorAt(
463
+ "<ul><li><p>1</p><ul><li><p>c</p></li><li><p></p></li></ul></li><li><p>2</p></li></ul>",
464
+ "",
465
+ "start",
466
+ );
401
467
 
402
- it("does not lift a whole item from a paragraph after a nested list", () => {
403
- const editor = createEditor(
404
- "<ul><li><p>A</p><ul><li><p>Nested</p></li></ul><p>Continuation</p></li><li><p>B</p></li></ul>",
405
- );
406
- setCursor(editor, paragraphPosition(editor, "Continuation", "start"));
407
- const before = editor.getJSON();
468
+ expect(pressKey(editor, "Backspace")).toBe(true);
469
+ expect(pressKey(editor, "Backspace")).toBe(true);
470
+ expect(outline(editor)).toBe(["- 1", " - c|", "- 2"].join("\n"));
471
+ });
408
472
 
409
- expect(pressKey(editor, "Backspace")).toBe(true);
410
- expect(editor.getJSON()).toEqual(before);
473
+ it("merges a paragraph after a nested list into the line above", () => {
474
+ const editor = editorAt(
475
+ "<ul><li><p>A</p><ul><li><p>Nested</p></li></ul><p>Continuation</p></li><li><p>B</p></li></ul>",
476
+ "Continuation",
477
+ "start",
478
+ );
479
+
480
+ expect(pressKey(editor, "Backspace")).toBe(true);
481
+ expect(outline(editor)).toBe(
482
+ ["- A", " - Nested|Continuation", "- B"].join("\n"),
483
+ );
484
+ });
485
+
486
+ it("restores list items and subtrees with undo", () => {
487
+ const editor = editorAt(
488
+ "<ul><li><p>A</p></li><li><p>B</p><ul><li><p>Child</p></li></ul></li></ul>",
489
+ "B",
490
+ "start",
491
+ );
492
+ const before = editor.getJSON();
493
+
494
+ expect(pressKey(editor, "Backspace")).toBe(true);
495
+ expect(editor.commands.undo()).toBe(true);
496
+ expect(editor.getJSON()).toEqual(before);
497
+ });
411
498
  });
412
499
 
413
- it("lets the official keymap promote a first nested item", () => {
414
- const editor = createEditor(
415
- "<ul><li><p>Parent</p><ul><li><p>Child</p></li></ul></li></ul>",
500
+ describe("Delete at the end of a line", () => {
501
+ it.each([
502
+ ["ordered", "ol", ["1. A|B", " - y"]],
503
+ ["bullet", "ul", ["- A|B", " - y"]],
504
+ ])(
505
+ "merges the next %s item and keeps its children",
506
+ (_name, tag, lines) => {
507
+ const editor = editorAt(
508
+ `<${tag}><li><p>A</p></li><li><p>B</p><ul><li><p>y</p></li></ul></li></${tag}>`,
509
+ "A",
510
+ "end",
511
+ );
512
+
513
+ expect(pressKey(editor, "Delete")).toBe(true);
514
+ expect(outline(editor)).toBe(lines.join("\n"));
515
+ },
416
516
  );
417
- setCursor(editor, paragraphPosition(editor, "Child", "start"));
418
517
 
419
- expect(pressKey(editor, "Backspace")).toBe(true);
518
+ it("pulls a shallower item up without moving its children", () => {
519
+ const editor = editorAt(
520
+ "<ul><li><p>1</p><ul><li><p>a</p></li><li><p>c</p></li></ul></li><li><p>2</p><ul><li><p>x</p></li></ul></li><li><p>3</p></li></ul>",
521
+ "c",
522
+ "end",
523
+ );
420
524
 
421
- const list = editor.state.doc.firstChild;
422
- expect(list?.type.name).toBe("bulletList");
423
- expect(list?.childCount).toBe(2);
424
- expect(list?.child(0).textContent).toBe("Parent");
425
- expect(list?.child(1).textContent).toBe("Child");
525
+ expect(pressKey(editor, "Delete")).toBe(true);
526
+ expect(outline(editor)).toBe(
527
+ ["- 1", " - a", " - c|2", " - x", "- 3"].join("\n"),
528
+ );
529
+ });
530
+
531
+ it("pulls a first nested item up and moves its children up one level", () => {
532
+ const editor = editorAt(
533
+ "<ul><li><p>P</p><ul><li><p>c1</p><ul><li><p>y</p></li></ul></li><li><p>c2</p></li></ul></li></ul>",
534
+ "P",
535
+ "end",
536
+ );
537
+
538
+ expect(pressKey(editor, "Delete")).toBe(true);
539
+ expect(outline(editor)).toBe(["- P|c1", " - y", " - c2"].join("\n"));
540
+ });
541
+
542
+ it("pulls a list's first item into the paragraph above", () => {
543
+ const editor = editorAt(
544
+ "<p>intro</p><ul><li><p>A</p><ul><li><p>k</p></li></ul></li><li><p>B</p></li></ul>",
545
+ "intro",
546
+ "end",
547
+ );
548
+
549
+ expect(pressKey(editor, "Delete")).toBe(true);
550
+ expect(outline(editor)).toBe(["intro|A", "- k", "- B"].join("\n"));
551
+ });
552
+
553
+ it("agrees with Backspace for a line without a marker", () => {
554
+ const html =
555
+ "<ul><li><p>A</p><ul><li><p>c</p></li></ul></li></ul><p>tail</p>";
556
+ const forward = editorAt(html, "c", "end");
557
+ const backward = editorAt(html, "tail", "start");
558
+
559
+ expect(pressKey(forward, "Delete")).toBe(true);
560
+ expect(pressKey(backward, "Backspace")).toBe(true);
561
+ expect(outline(forward)).toBe(["- A", " - c|tail"].join("\n"));
562
+ expect(outline(backward)).toBe(outline(forward));
563
+ });
426
564
  });
427
565
 
428
566
  it("preserves multiple paragraphs and a nested list when pasting HTML", () => {
@@ -3,7 +3,6 @@
3
3
  import { afterEach, describe, expect, it } from "vitest";
4
4
  import { Editor } from "@tiptap/core";
5
5
  import { createMarkdownContentExtensions } from "../../../lib/markdown-manager.js";
6
- import { ExitableMarks } from "../exitable-marks.js";
7
6
  import { toggleMarkAndExit } from "../bubble-menu.js";
8
7
 
9
8
  const editors: Editor[] = [];
@@ -13,7 +12,7 @@ function createEditor(content: string): Editor {
13
12
  document.body.appendChild(element);
14
13
  const editor = new Editor({
15
14
  element,
16
- extensions: [...createMarkdownContentExtensions(), ExitableMarks],
15
+ extensions: createMarkdownContentExtensions(),
17
16
  content,
18
17
  });
19
18
  editor.view.dispatch(editor.state.tr);
@@ -150,6 +150,32 @@ describe("isCodeEditorHtml", () => {
150
150
  });
151
151
 
152
152
  describe("MarkdownClipboard", () => {
153
+ it("keeps pasted plain text that opens with a word and a period", () => {
154
+ const element = document.createElement("div");
155
+ document.body.appendChild(element);
156
+ const editor = createTiptapEditor({ element });
157
+ editors.push(editor);
158
+
159
+ dispatchMarkdownPaste(editor, "PS. 补充一句");
160
+
161
+ expect(editor.getJSON().content).toEqual([
162
+ { type: "paragraph", content: [{ type: "text", text: "PS. 补充一句" }] },
163
+ ]);
164
+ });
165
+
166
+ it("turns pasted plain numbered lines into an ordered list", () => {
167
+ const element = document.createElement("div");
168
+ document.body.appendChild(element);
169
+ const editor = createTiptapEditor({ element });
170
+ editors.push(editor);
171
+
172
+ dispatchMarkdownPaste(editor, "1. First\n2. Second");
173
+
174
+ const list = editor.getJSON().content?.[0];
175
+ expect(list?.type).toBe("orderedList");
176
+ expect(list?.content).toHaveLength(2);
177
+ });
178
+
153
179
  it("prefers an explicit text/markdown flavor over HTML and plain text", () => {
154
180
  const element = document.createElement("div");
155
181
  document.body.appendChild(element);
@@ -12,7 +12,6 @@ import { PasteMedia } from "./paste-media.js";
12
12
  import type { PasteMediaOptions } from "./paste-media.js";
13
13
  import { BubbleMenu } from "./bubble-menu.js";
14
14
  import { LinkToolbar } from "./link-toolbar.js";
15
- import { ExitableMarks } from "./exitable-marks.js";
16
15
  import { CodeBlockIndent } from "./tab-indent.js";
17
16
  import { StructuralKeymap } from "./structural-keymap.js";
18
17
  import { ContinuousLists } from "./continuous-lists.js";
@@ -88,7 +87,6 @@ export function createSettingsEditorExtensions(
88
87
  ReclaimModEnter,
89
88
  LinkInputRules,
90
89
  MarkdownClipboard,
91
- ExitableMarks,
92
90
  InsertParagraphAround,
93
91
  StructuralKeymap,
94
92
  ContinuousLists,
@@ -133,7 +131,6 @@ export function createEditorExtensions(
133
131
  toolbarMode: options.toolbarMode ?? "default",
134
132
  }),
135
133
  LinkToolbar,
136
- ExitableMarks,
137
134
  InsertParagraphAround,
138
135
  StructuralKeymap,
139
136
  ContinuousLists,