@jant/core 0.6.12 → 0.6.13

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 (106) hide show
  1. package/bin/commands/import-site.js +2 -10
  2. package/bin/commands/migrate-text-attachments.js +1 -3
  3. package/bin/commands/site/export.js +1 -3
  4. package/bin/commands/telegram/register-webhooks.js +9 -3
  5. package/bin/lib/site-snapshot.js +1 -5
  6. package/dist/app-CWJFPjuR.js +6 -0
  7. package/dist/{app-D-zAhayc.js → app-HnzmsaqU.js} +416 -444
  8. package/dist/client/.vite/manifest.json +3 -3
  9. package/dist/client/_assets/{client-S1cdvulk.js → client-3_OaxhTs.js} +1 -1
  10. package/dist/client/_assets/{client-BUxR-E8O.css → client-DCrcHVJl.css} +1 -1
  11. package/dist/client/_assets/{client-auth-YW92ZH7u.js → client-auth-TsYq90xm.js} +570 -567
  12. package/dist/{export-Ckwh4BiE.js → export-Dfbq9aot.js} +10 -13
  13. package/dist/{github-sync-Dl9wQMar.js → github-sync--PSoE-Ne.js} +1 -1
  14. package/dist/{github-sync-BMmK3JZ0.js → github-sync-BGTLx8Mf.js} +2 -2
  15. package/dist/index.js +3 -3
  16. package/dist/node.js +4 -4
  17. package/package.json +1 -1
  18. package/src/__tests__/export-service.test.ts +1 -2
  19. package/src/__tests__/helpers/app.ts +3 -0
  20. package/src/__tests__/helpers/export-fixtures.ts +0 -1
  21. package/src/__tests__/import-site-command.test.ts +1 -2
  22. package/src/client/__tests__/site-header-nav.test.ts +36 -2
  23. package/src/client/components/__tests__/compose-title-from-heading.test.ts +57 -0
  24. package/src/client/components/__tests__/jant-compose-editor.test.ts +94 -2
  25. package/src/client/components/__tests__/jant-nav-manager.test.ts +107 -22
  26. package/src/client/components/__tests__/jant-settings-general.test.ts +1 -0
  27. package/src/client/components/compose-title-from-heading.ts +66 -0
  28. package/src/client/components/compose-types.ts +8 -0
  29. package/src/client/components/inline-image-issues.ts +17 -0
  30. package/src/client/components/jant-compose-dialog.ts +2 -0
  31. package/src/client/components/jant-compose-editor.ts +78 -1
  32. package/src/client/components/jant-compose-fullscreen.ts +2 -0
  33. package/src/client/components/jant-nav-manager.ts +245 -155
  34. package/src/client/components/nav-manager-types.ts +0 -10
  35. package/src/client/site-header-nav.d.ts +1 -0
  36. package/src/client/site-header-nav.js +105 -47
  37. package/src/client/tiptap/__tests__/block-insertion.test.ts +35 -0
  38. package/src/client/tiptap/__tests__/image-input-rules.test.ts +117 -0
  39. package/src/client/tiptap/create-editor.ts +7 -0
  40. package/src/client/tiptap/extensions.ts +7 -2
  41. package/src/client/tiptap/image-input-rules.ts +48 -0
  42. package/src/client/tiptap/image-node.ts +189 -7
  43. package/src/client-auth.ts +0 -1
  44. package/src/i18n/locales/public/en.po +20 -4
  45. package/src/i18n/locales/public/en.ts +1 -1
  46. package/src/i18n/locales/public/zh-Hans.po +20 -4
  47. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  48. package/src/i18n/locales/public/zh-Hant.po +20 -4
  49. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  50. package/src/i18n/locales/settings/en.po +23 -20
  51. package/src/i18n/locales/settings/en.ts +1 -1
  52. package/src/i18n/locales/settings/zh-Hans.po +23 -20
  53. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  54. package/src/i18n/locales/settings/zh-Hant.po +23 -20
  55. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  56. package/src/lib/__tests__/navigation.test.ts +4 -37
  57. package/src/lib/__tests__/resolve-config.test.ts +6 -1
  58. package/src/lib/__tests__/view.test.ts +6 -30
  59. package/src/lib/github-sync-site-config.ts +0 -2
  60. package/src/lib/hugo-markdown.ts +1 -1
  61. package/src/lib/navigation.ts +5 -22
  62. package/src/lib/resolve-config.ts +9 -2
  63. package/src/lib/site-header-fragment.tsx +37 -0
  64. package/src/lib/view.ts +2 -7
  65. package/src/node/__tests__/cli-site-snapshot.test.ts +45 -45
  66. package/src/routes/api/__tests__/nav-items.test.ts +26 -0
  67. package/src/routes/api/export.ts +0 -2
  68. package/src/routes/api/internal/__tests__/uploads.test.ts +9 -13
  69. package/src/routes/api/internal/sites.ts +5 -5
  70. package/src/routes/api/nav-items.ts +25 -4
  71. package/src/routes/dash/settings.tsx +8 -2
  72. package/src/routes/feed/__tests__/feed.test.ts +42 -0
  73. package/src/routes/feed/__tests__/sitemap.test.ts +2 -13
  74. package/src/routes/feed/feed.ts +1 -0
  75. package/src/routes/feed/sitemap.ts +2 -6
  76. package/src/routes/pages/archive.tsx +1 -0
  77. package/src/routes/pages/collection.tsx +1 -0
  78. package/src/routes/pages/featured.tsx +0 -4
  79. package/src/routes/pages/home.tsx +4 -51
  80. package/src/routes/pages/latest.tsx +2 -51
  81. package/src/services/__tests__/github-app-installations.test.ts +10 -10
  82. package/src/services/__tests__/navigation.test.ts +41 -0
  83. package/src/services/__tests__/post-timeline.test.ts +56 -0
  84. package/src/services/__tests__/post.test.ts +33 -0
  85. package/src/services/__tests__/settings.test.ts +5 -20
  86. package/src/services/export-theme/assets/client-site.js +10 -10
  87. package/src/services/export-theme/layouts/_default/rss.xml +5 -15
  88. package/src/services/export-theme/layouts/index.html +26 -39
  89. package/src/services/export.ts +5 -10
  90. package/src/services/github-app-installations.ts +1 -3
  91. package/src/services/navigation.ts +44 -13
  92. package/src/services/post.ts +80 -57
  93. package/src/services/settings.ts +0 -10
  94. package/src/services/site-admin.ts +0 -2
  95. package/src/services/upload-session.ts +4 -18
  96. package/src/styles/tokens.css +2 -2
  97. package/src/styles/ui.css +200 -6
  98. package/src/types/bindings.ts +0 -1
  99. package/src/types/config.ts +3 -7
  100. package/src/ui/compose/ComposeDialog.tsx +28 -0
  101. package/src/ui/dash/appearance/NavigationContent.tsx +1 -1
  102. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  103. package/src/ui/dash/settings/SettingsRootContent.tsx +3 -3
  104. package/src/ui/layouts/SiteLayout.tsx +249 -216
  105. package/dist/app-B5P1b4TF.js +0 -6
  106. package/src/client/nav-manager-bridge.ts +0 -54
@@ -5,6 +5,9 @@
5
5
  * Manages the "More" dropdown popover.
6
6
  */
7
7
 
8
+ const moreControllers = new WeakMap();
9
+ const drawerControllers = new WeakMap();
10
+
8
11
  function initMoreDropdown(root) {
9
12
  const trigger = root.querySelector(".site-header-more-btn");
10
13
  const popover = root.querySelector(".site-header-more-popover");
@@ -12,6 +15,9 @@ function initMoreDropdown(root) {
12
15
  if (!trigger || !popover) return;
13
16
  if (trigger.dataset.moreInitialized === "true") return;
14
17
  trigger.dataset.moreInitialized = "true";
18
+ const controller = new AbortController();
19
+ const signal = controller.signal;
20
+ moreControllers.set(trigger, controller);
15
21
 
16
22
  function open() {
17
23
  popover.setAttribute("aria-hidden", "false");
@@ -29,30 +35,49 @@ function initMoreDropdown(root) {
29
35
  if (focusTrigger) trigger.focus();
30
36
  }
31
37
 
32
- trigger.addEventListener("click", (e) => {
33
- e.preventDefault();
34
- e.stopPropagation();
35
- if (trigger.getAttribute("aria-expanded") === "true") {
36
- close();
37
- } else {
38
- open();
39
- }
40
- });
41
-
42
- document.addEventListener("click", (e) => {
43
- if (!(e.target instanceof Node)) return;
44
- if (!trigger.parentElement?.contains(e.target)) close();
45
- });
46
-
47
- document.addEventListener("keydown", (e) => {
48
- if (e.key === "Escape" && popover.getAttribute("aria-hidden") === "false") {
49
- close(true);
50
- }
51
- });
52
-
53
- document.addEventListener("basecoat:popover", (e) => {
54
- if (e.detail?.source !== trigger.parentElement) close();
55
- });
38
+ trigger.addEventListener(
39
+ "click",
40
+ (e) => {
41
+ e.preventDefault();
42
+ e.stopPropagation();
43
+ if (trigger.getAttribute("aria-expanded") === "true") {
44
+ close();
45
+ } else {
46
+ open();
47
+ }
48
+ },
49
+ { signal },
50
+ );
51
+
52
+ document.addEventListener(
53
+ "click",
54
+ (e) => {
55
+ if (!(e.target instanceof Node)) return;
56
+ if (!trigger.parentElement?.contains(e.target)) close();
57
+ },
58
+ { signal },
59
+ );
60
+
61
+ document.addEventListener(
62
+ "keydown",
63
+ (e) => {
64
+ if (
65
+ e.key === "Escape" &&
66
+ popover.getAttribute("aria-hidden") === "false"
67
+ ) {
68
+ close(true);
69
+ }
70
+ },
71
+ { signal },
72
+ );
73
+
74
+ document.addEventListener(
75
+ "basecoat:popover",
76
+ (e) => {
77
+ if (e.detail?.source !== trigger.parentElement) close();
78
+ },
79
+ { signal },
80
+ );
56
81
  }
57
82
 
58
83
  const FRESH_VISIT_KEY = "jant:nav-fresh-visits";
@@ -111,6 +136,9 @@ export function initSiteHeaderNav(root = document) {
111
136
  if (!hamburger || !drawer || !backdrop) return;
112
137
  if (hamburger.dataset.drawerInitialized === "true") return;
113
138
  hamburger.dataset.drawerInitialized = "true";
139
+ const controller = new AbortController();
140
+ const signal = controller.signal;
141
+ drawerControllers.set(hamburger, controller);
114
142
 
115
143
  function open() {
116
144
  drawer.setAttribute("aria-hidden", "false");
@@ -138,35 +166,65 @@ export function initSiteHeaderNav(root = document) {
138
166
  drawer.setAttribute("inert", "");
139
167
  }
140
168
  },
141
- { once: true },
169
+ { once: true, signal },
142
170
  );
143
171
 
144
172
  if (returnFocus) hamburger.focus();
145
173
  }
146
174
 
147
- hamburger.addEventListener("click", () => {
148
- if (hamburger.getAttribute("aria-expanded") === "true") {
149
- close();
150
- } else {
151
- open();
152
- }
153
- });
154
-
155
- closeBtn?.addEventListener("click", () => close());
156
- backdrop.addEventListener("click", () => close());
157
- drawer.addEventListener("click", (event) => {
158
- if (!(event.target instanceof Element)) return;
159
- if (event.target.closest("a[href]")) {
160
- close(false);
161
- }
162
- });
175
+ hamburger.addEventListener(
176
+ "click",
177
+ () => {
178
+ if (hamburger.getAttribute("aria-expanded") === "true") {
179
+ close();
180
+ } else {
181
+ open();
182
+ }
183
+ },
184
+ { signal },
185
+ );
186
+
187
+ closeBtn?.addEventListener("click", () => close(), { signal });
188
+ backdrop.addEventListener("click", () => close(), { signal });
189
+ drawer.addEventListener(
190
+ "click",
191
+ (event) => {
192
+ if (!(event.target instanceof Element)) return;
193
+ if (event.target.closest("a[href]")) {
194
+ close(false);
195
+ }
196
+ },
197
+ { signal },
198
+ );
199
+
200
+ drawer.addEventListener(
201
+ "keydown",
202
+ (event) => {
203
+ if (event.key === "Escape") {
204
+ event.preventDefault();
205
+ close();
206
+ }
207
+ },
208
+ { signal },
209
+ );
210
+ }
163
211
 
164
- drawer.addEventListener("keydown", (event) => {
165
- if (event.key === "Escape") {
166
- event.preventDefault();
167
- close();
168
- }
169
- });
212
+ export function destroySiteHeaderNav(root = document) {
213
+ for (const trigger of root.querySelectorAll(".site-header-more-btn")) {
214
+ const controller = moreControllers.get(trigger);
215
+ controller?.abort();
216
+ moreControllers.delete(trigger);
217
+ delete trigger.dataset.moreInitialized;
218
+ }
219
+
220
+ for (const hamburger of root.querySelectorAll(".site-header-hamburger")) {
221
+ const controller = drawerControllers.get(hamburger);
222
+ controller?.abort();
223
+ drawerControllers.delete(hamburger);
224
+ delete hamburger.dataset.drawerInitialized;
225
+ }
226
+
227
+ document.documentElement.classList.remove("drawer-open");
170
228
  }
171
229
 
172
230
  initSiteHeaderNav();
@@ -62,4 +62,39 @@ describe("Block insertion flow", () => {
62
62
  expect(editor.state.selection.$from.parent.type.name).toBe("paragraph");
63
63
  expect(editor.state.selection.$from.parentOffset).toBe(0);
64
64
  });
65
+
66
+ it("renders a removable placeholder when an image fails to load", () => {
67
+ const editor = createEditor();
68
+
69
+ editor.commands.focus("start");
70
+ expect(editor.commands.setImage({ src: "/uploads/missing.webp" })).toBe(
71
+ true,
72
+ );
73
+
74
+ const figure = document.querySelector<HTMLElement>(".tiptap-image-figure");
75
+ const img = figure?.querySelector<HTMLImageElement>("img");
76
+ if (!figure || !img) {
77
+ throw new Error("expected image node view");
78
+ }
79
+
80
+ img.dispatchEvent(new Event("error"));
81
+
82
+ expect(figure.dataset.loadState).toBe("missing");
83
+ expect(figure.textContent).toContain("Image unavailable");
84
+
85
+ const deleteButton = figure.querySelector<HTMLButtonElement>(
86
+ ".tiptap-image-missing-delete",
87
+ );
88
+ if (!deleteButton) {
89
+ throw new Error("expected broken image delete button");
90
+ }
91
+
92
+ deleteButton.dispatchEvent(
93
+ new MouseEvent("mousedown", { bubbles: true, cancelable: true }),
94
+ );
95
+
96
+ expect(
97
+ editor.getJSON().content?.some((node) => node.type === "image"),
98
+ ).toBe(false);
99
+ });
65
100
  });
@@ -0,0 +1,117 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { afterEach, describe, expect, it } from "vitest";
4
+ import { Editor } from "@tiptap/core";
5
+ import StarterKit from "@tiptap/starter-kit";
6
+ import { ImageInputRules } from "../image-input-rules.js";
7
+ import { ImageNode } from "../image-node.js";
8
+ import { LinkInputRules } from "../link-input-rules.js";
9
+
10
+ const editors: Editor[] = [];
11
+
12
+ function createEditor(): Editor {
13
+ const element = document.createElement("div");
14
+ document.body.appendChild(element);
15
+
16
+ const editor = new Editor({
17
+ element,
18
+ extensions: [
19
+ StarterKit.configure({
20
+ heading: { levels: [1, 2, 3] },
21
+ link: { openOnClick: false, autolink: false },
22
+ }),
23
+ LinkInputRules,
24
+ ImageNode,
25
+ ImageInputRules,
26
+ ],
27
+ content: "<p></p>",
28
+ });
29
+
30
+ editors.push(editor);
31
+ return editor;
32
+ }
33
+
34
+ function type(editor: Editor, text: string): void {
35
+ const view = editor.view;
36
+ for (const ch of text) {
37
+ const { from, to } = view.state.selection;
38
+ const handled = view.someProp("handleTextInput", (handler) =>
39
+ handler(view, from, to, ch, () => view.state.tr.insertText(ch, from, to)),
40
+ );
41
+ if (!handled) view.dispatch(view.state.tr.insertText(ch, from, to));
42
+ }
43
+ }
44
+
45
+ afterEach(() => {
46
+ while (editors.length > 0) {
47
+ editors.pop()?.destroy();
48
+ }
49
+ document.body.innerHTML = "";
50
+ });
51
+
52
+ describe("ImageInputRules", () => {
53
+ it("converts typed empty-alt Markdown image syntax into an image node", () => {
54
+ const editor = createEditor();
55
+
56
+ type(editor, "![](https://example.com/image.png)");
57
+
58
+ expect(editor.getJSON().content).toEqual([
59
+ {
60
+ type: "image",
61
+ attrs: {
62
+ src: "https://example.com/image.png",
63
+ alt: "",
64
+ title: "",
65
+ caption: "",
66
+ href: "",
67
+ layout: "regular",
68
+ },
69
+ },
70
+ { type: "paragraph" },
71
+ ]);
72
+ expect(editor.state.selection.$from.parent.type.name).toBe("paragraph");
73
+ });
74
+
75
+ it("preserves alt text and optional quoted title", () => {
76
+ const editor = createEditor();
77
+
78
+ type(editor, '![A test image](https://example.com/image.png "Title")');
79
+
80
+ expect(editor.getJSON().content?.[0]).toMatchObject({
81
+ type: "image",
82
+ attrs: {
83
+ src: "https://example.com/image.png",
84
+ alt: "A test image",
85
+ title: "Title",
86
+ },
87
+ });
88
+ });
89
+
90
+ it("runs before Markdown link rules", () => {
91
+ const editor = createEditor();
92
+
93
+ type(editor, "![Alt](https://example.com/image.png)");
94
+
95
+ expect(editor.getJSON().content?.[0]).toMatchObject({
96
+ type: "image",
97
+ attrs: {
98
+ src: "https://example.com/image.png",
99
+ alt: "Alt",
100
+ },
101
+ });
102
+ expect(editor.state.doc.textContent).toBe("");
103
+ });
104
+
105
+ it("leaves unsafe image URLs as typed text", () => {
106
+ const editor = createEditor();
107
+
108
+ type(editor, "![](javascript:alert)");
109
+
110
+ expect(editor.getJSON().content).toEqual([
111
+ {
112
+ type: "paragraph",
113
+ content: [{ type: "text", text: "![](javascript:alert)" }],
114
+ },
115
+ ]);
116
+ });
117
+ });
@@ -12,6 +12,7 @@ import {
12
12
  import type { FormattingToolbarMode } from "./toolbar-mode.js";
13
13
  import type { PasteMediaOptions } from "./paste-media.js";
14
14
  import type { RehostImagesOptions } from "./rehost-images.js";
15
+ import type { ImageNodeLabels } from "./image-node.js";
15
16
  import { normalizeFootnoteArtifacts } from "../../lib/footnotes.js";
16
17
  import { tiptapJsonToMarkdown } from "../../lib/tiptap-to-markdown.js";
17
18
  import { parseMarkdownDocument } from "../../lib/markdown-manager.js";
@@ -22,10 +23,12 @@ export interface CreateEditorOptions {
22
23
  content?: JSONContent | null;
23
24
  onUpdate?: (json: JSONContent) => void;
24
25
  onFocus?: () => void;
26
+ onBlur?: () => void;
25
27
  onSelectionUpdate?: (selection: { from: number; to: number }) => void;
26
28
  toolbarMode?: FormattingToolbarMode;
27
29
  pasteMedia?: PasteMediaOptions;
28
30
  rehostImages?: RehostImagesOptions;
31
+ imageNodeLabels?: Partial<ImageNodeLabels>;
29
32
  }
30
33
 
31
34
  /**
@@ -42,6 +45,7 @@ export function createTiptapEditor(options: CreateEditorOptions): Editor {
42
45
  toolbarMode: options.toolbarMode,
43
46
  pasteMedia: options.pasteMedia,
44
47
  rehostImages: options.rehostImages,
48
+ imageNodeLabels: options.imageNodeLabels,
45
49
  }),
46
50
  content: options.content ?? undefined,
47
51
  editorProps: {
@@ -54,6 +58,9 @@ export function createTiptapEditor(options: CreateEditorOptions): Editor {
54
58
  onFocus: () => {
55
59
  options.onFocus?.();
56
60
  },
61
+ onBlur: () => {
62
+ options.onBlur?.();
63
+ },
57
64
  onSelectionUpdate: ({ editor }) => {
58
65
  options.onSelectionUpdate?.({
59
66
  from: editor.state.selection.from,
@@ -19,7 +19,7 @@ import { WrappingInputRules } from "./wrapping-input-rules.js";
19
19
  import { InsertParagraphAround } from "./insert-paragraph-around.js";
20
20
  import { Footnotes } from "./footnotes.js";
21
21
  import type { FormattingToolbarMode } from "./toolbar-mode.js";
22
- import { ImageNode } from "./image-node.js";
22
+ import { ImageNode, type ImageNodeLabels } from "./image-node.js";
23
23
  import { MoreBreak } from "./more-break.js";
24
24
  import { EmbedNode } from "./embed-node.js";
25
25
  import { HtmlBlockNode } from "./html-block-node.js";
@@ -27,6 +27,7 @@ import { EmbedPaste } from "./embed-paste.js";
27
27
  import { RehostImages } from "./rehost-images.js";
28
28
  import type { RehostImagesOptions } from "./rehost-images.js";
29
29
  import { MarkdownClipboard } from "./markdown-clipboard.js";
30
+ import { ImageInputRules } from "./image-input-rules.js";
30
31
  import {
31
32
  MARKDOWN_MARKED_OPTIONS,
32
33
  createMarkdownContentExtensions,
@@ -37,6 +38,7 @@ export interface EditorExtensionOptions {
37
38
  toolbarMode?: FormattingToolbarMode;
38
39
  pasteMedia?: PasteMediaOptions;
39
40
  rehostImages?: RehostImagesOptions;
41
+ imageNodeLabels?: Partial<ImageNodeLabels>;
40
42
  }
41
43
 
42
44
  /**
@@ -97,7 +99,9 @@ export function createEditorExtensions(
97
99
  ): Extensions {
98
100
  return [
99
101
  ...createMarkdownContentExtensions({
100
- imageExtension: ImageNode,
102
+ imageExtension: ImageNode.configure({
103
+ labels: options.imageNodeLabels,
104
+ }),
101
105
  moreBreakExtension: MoreBreak,
102
106
  embedExtension: EmbedNode,
103
107
  htmlBlockExtension: HtmlBlockNode,
@@ -110,6 +114,7 @@ export function createEditorExtensions(
110
114
  placeholder: options.placeholder ?? "Write something…",
111
115
  }),
112
116
  Footnotes,
117
+ ImageInputRules,
113
118
  LinkInputRules,
114
119
  WrappingInputRules,
115
120
  MarkdownClipboard,
@@ -0,0 +1,48 @@
1
+ import { Extension, InputRule } from "@tiptap/core";
2
+ import { sanitizeUrl } from "../../lib/url.js";
3
+ import { moveSelectionAfterBlockInsertion } from "./block-insertion.js";
4
+
5
+ // Keep the URL group aligned with link-input-rules: single-character
6
+ // alternatives avoid catastrophic backtracking on failed Markdown matches.
7
+ const MARKDOWN_IMAGE_INPUT_REGEX =
8
+ /!\[([^\]\n]*)\]\(((?:[^()\s"]|\([^()\s"]*\))+)(?:\s*"([^"\n]*)")?\)$/;
9
+
10
+ function sanitizeImageSrc(src: string): string {
11
+ const sanitized = sanitizeUrl(src.trim());
12
+ if (!sanitized || /^mailto:/i.test(sanitized)) return "";
13
+ return sanitized;
14
+ }
15
+
16
+ export const ImageInputRules = Extension.create({
17
+ name: "imageInputRules",
18
+
19
+ // Runs before LinkInputRules so `![alt](url)` is handled as an image rather
20
+ // than the inner `[alt](url)` being converted to a link.
21
+ priority: 1000,
22
+
23
+ addInputRules() {
24
+ const imageType = this.editor.schema.nodes.image;
25
+ if (!imageType) return [];
26
+
27
+ return [
28
+ new InputRule({
29
+ find: MARKDOWN_IMAGE_INPUT_REGEX,
30
+ handler: ({ state, range, match }) => {
31
+ const src = sanitizeImageSrc(match[2] ?? "");
32
+ if (!src) return null;
33
+
34
+ state.tr.replaceRangeWith(
35
+ range.from,
36
+ range.to,
37
+ imageType.create({
38
+ src,
39
+ alt: match[1] ?? "",
40
+ title: match[3] ?? "",
41
+ }),
42
+ );
43
+ moveSelectionAfterBlockInsertion(state.tr, state.schema);
44
+ },
45
+ }),
46
+ ];
47
+ },
48
+ });