@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.
- package/bin/commands/export.js +3 -1
- package/bin/commands/import-site.js +689 -216
- package/bin/commands/setup.js +136 -0
- package/bin/commands/site/export.js +71 -34
- package/bin/commands/site/pull-media.js +2 -6
- package/bin/commands/site/snapshot/export.js +18 -60
- package/bin/commands/site/snapshot/import.js +22 -23
- package/bin/lib/d1-query.js +87 -2
- package/bin/lib/hugo-markdown.js +4 -0
- package/bin/lib/site-pull-media.js +21 -28
- package/bin/lib/site-selection.js +10 -1
- package/bin/lib/site-snapshot.js +338 -3
- package/bin/lib/sql-export.js +68 -5
- package/bin/lib/wrangler-cli.js +9 -0
- package/bin/lib/zip-archive.js +187 -0
- package/dist/{app-K_Aa1MMn.js → app-ZcaI1kPN.js} +732 -217
- package/dist/client/.vite/manifest.json +20 -20
- package/dist/client/_assets/chunks/{create-editor-CD3FhrOB.js → create-editor-B-m7X7S5.js} +50 -46
- package/dist/client/_assets/chunks/{sortable-list-CgaL2jCs.js → sortable-list-BJyd-LXE.js} +1 -1
- package/dist/client/_assets/chunks/{unsafe-svg-0QCkP0vZ.js → unsafe-svg-BkscJx69.js} +2 -2
- package/dist/client/_assets/{client-GiYENVw8.css → client-DAhoqPdr.css} +1 -1
- package/dist/client/_assets/{client-auth-k8gJ6QJj.js → client-auth-CAMfTrKW.js} +1 -1
- package/dist/client/_assets/{client-compose-B_kDtWkW.js → client-compose-D6rExKVK.js} +1 -1
- package/dist/client/_assets/{client-VnFxJN7G.js → client-knSEyUJO.js} +1 -1
- package/dist/client/_assets/{client-manage-M90aSTOk.js → client-manage-sVOkqVbH.js} +1 -1
- package/dist/client/_assets/{client-settings-DfYs9n1F.js → client-settings-CGR_nZ78.js} +7 -7
- package/dist/{github-sync-BPAvT999.js → github-sync-Gw4orAHk.js} +799 -110
- package/dist/index.js +2 -2
- package/dist/node.js +9 -5
- package/package.json +13 -3
- package/src/__tests__/dev-scripts.test.ts +203 -0
- package/src/__tests__/export-collection-order.test.ts +276 -0
- package/src/__tests__/export-feed-ids.test.ts +184 -0
- package/src/__tests__/export-feed-order.test.ts +294 -0
- package/src/__tests__/export-hugo-build.test.ts +130 -0
- package/src/__tests__/export-import-roundtrip.test.ts +94 -0
- package/src/__tests__/export-service.test.ts +611 -28
- package/src/__tests__/export-smart-collection.test.ts +329 -0
- package/src/__tests__/helpers/hugo-site.ts +146 -0
- package/src/__tests__/import-site-command.test.ts +487 -1
- package/src/__tests__/mise-config.test.ts +75 -4
- package/src/__tests__/node-dev-tasks.test.ts +264 -0
- package/src/__tests__/site-export-canonical-import.test.ts +149 -0
- package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
- package/src/__tests__/snapshot-settings.test.ts +97 -0
- package/src/__tests__/snapshot-tables.test.ts +219 -0
- package/src/__tests__/sql-export.test.ts +173 -0
- package/src/__tests__/zip-archive.test.ts +106 -0
- package/src/app.tsx +15 -6
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +1 -1
- package/src/client/components/__tests__/jant-settings-general.test.ts +21 -1
- package/src/client/components/jant-repo-picker-types.ts +6 -1
- package/src/client/components/jant-repo-picker.ts +4 -7
- package/src/client/components/jant-settings-general.ts +17 -12
- package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
- package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
- package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
- package/src/client/tiptap/extensions.ts +0 -3
- package/src/client/tiptap/structural-keymap.ts +158 -47
- package/src/db/__tests__/d1-query.test.ts +56 -1
- package/src/i18n/locales/settings/en.po +8 -8
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +8 -8
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +8 -8
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
- package/src/lib/__tests__/image.test.ts +27 -1
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
- package/src/lib/__tests__/markdown.test.ts +10 -0
- package/src/lib/__tests__/resolve-config.test.ts +67 -0
- package/src/lib/__tests__/schemas.test.ts +27 -1
- package/src/lib/__tests__/timeline.test.ts +87 -0
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
- package/src/lib/discover.ts +3 -1
- package/src/lib/github-sync-repo-name.ts +45 -0
- package/src/lib/hugo-markdown.ts +46 -0
- package/src/lib/image.ts +17 -4
- package/src/lib/markdown-manager.ts +392 -2
- package/src/lib/post-body-html.ts +11 -4
- package/src/lib/resolve-config.ts +58 -1
- package/src/lib/schemas.ts +50 -5
- package/src/lib/thread-fold.ts +3 -3
- package/src/lib/timeline.ts +1 -1
- package/src/lib/tiptap-to-markdown.ts +13 -7
- package/src/lib/url.ts +20 -0
- package/src/lib/view.ts +2 -2
- package/src/node/__tests__/cli-setup.test.ts +163 -0
- package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
- package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
- package/src/node/__tests__/runtime.test.ts +38 -0
- package/src/node/index.ts +2 -0
- package/src/node/request-handler.ts +3 -1
- package/src/routes/api/__tests__/posts.test.ts +24 -0
- package/src/routes/api/__tests__/upload.test.ts +34 -0
- package/src/routes/api/export.ts +3 -3
- package/src/routes/api/internal/sites.ts +0 -1
- package/src/routes/api/posts.ts +2 -0
- package/src/routes/api/public/posts.ts +21 -1
- package/src/routes/api/upload.ts +10 -3
- package/src/routes/compose.tsx +4 -0
- package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
- package/src/routes/dash/settings.tsx +212 -70
- package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
- package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
- package/src/routes/pages/archive.tsx +17 -11
- package/src/routes/pages/featured.tsx +11 -5
- package/src/routes/pages/page.tsx +53 -31
- package/src/routes/pages/search.tsx +4 -2
- package/src/runtime/__tests__/readiness.test.ts +23 -0
- package/src/runtime/node.ts +49 -0
- package/src/runtime/readiness.ts +15 -0
- package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
- package/src/services/__tests__/custom-url.test.ts +34 -0
- package/src/services/__tests__/github-app-installations.test.ts +153 -0
- package/src/services/__tests__/github-sync-push.test.ts +46 -0
- package/src/services/__tests__/media.test.ts +31 -0
- package/src/services/__tests__/path.test.ts +56 -0
- package/src/services/__tests__/post-timeline.test.ts +127 -0
- package/src/services/__tests__/post.test.ts +74 -0
- package/src/services/bootstrap.ts +263 -44
- package/src/services/custom-url.ts +2 -2
- package/src/services/export-theme/layouts/_default/alias.html +27 -1
- package/src/services/export-theme/layouts/_default/list.html +2 -63
- package/src/services/export-theme/layouts/_default/rss.xml +27 -38
- package/src/services/export-theme/layouts/collections/list.html +3 -3
- package/src/services/export-theme/layouts/featured/list.html +1 -4
- package/src/services/export-theme/layouts/index.html +40 -26
- package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
- package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
- package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
- package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
- package/src/services/export-theme/layouts/partials/footer.html +1 -1
- package/src/services/export-theme/layouts/partials/head.html +1 -1
- package/src/services/export-theme/layouts/partials/header.html +5 -3
- package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
- package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
- package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
- package/src/services/export-theme/layouts/partials/thread-preview.html +1 -1
- package/src/services/export-theme/layouts/post/list.html +1 -1
- package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
- package/src/services/export-theme/styles/main.css +0 -1
- package/src/services/export-theme/theme.toml +1 -1
- package/src/services/export.ts +620 -71
- package/src/services/github-app-installations.ts +171 -4
- package/src/services/github-sync.ts +69 -15
- package/src/services/mcp.ts +19 -1
- package/src/services/media.ts +8 -1
- package/src/services/path.ts +34 -1
- package/src/services/post.ts +214 -39
- package/src/services/site-admin.ts +3 -7
- package/src/services/site.ts +83 -31
- package/src/styles/ui.css +7 -1
- package/src/types/app-context.ts +16 -0
- package/src/types/bindings.ts +5 -0
- package/src/types/operations.ts +10 -0
- package/src/ui/dash/settings/GeneralContent.tsx +10 -8
- 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
|
|
515
|
-
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
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.
|
|
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
|
|
1030
|
+
* The help line under the checkbox, with each page it names linking to that
|
|
1031
|
+
* page.
|
|
1030
1032
|
*
|
|
1031
|
-
*
|
|
1032
|
-
*
|
|
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
|
|
1039
|
+
private _renderDiscoverHelp(line: string) {
|
|
1035
1040
|
const runs = discoverIntroRuns(
|
|
1036
|
-
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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(
|
|
337
|
-
expect(
|
|
393
|
+
expect(pressKey(editor, "Backspace")).toBe(true);
|
|
394
|
+
expect(outline(editor)).toBe(["- 1", " - a", " - c", "|2"].join("\n"));
|
|
338
395
|
|
|
339
|
-
expect(
|
|
340
|
-
|
|
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
|
-
|
|
343
|
-
expect(
|
|
344
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
426
|
+
expect(pressKey(editor, "Backspace")).toBe(true);
|
|
427
|
+
expect(outline(editor)).toBe(["- 1", " - a|b", " - c"].join("\n"));
|
|
428
|
+
});
|
|
356
429
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
"
|
|
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
|
-
|
|
410
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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:
|
|
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,
|