@kungal/editor-core 0.34.0 → 0.36.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.
@@ -1,6 +1,6 @@
1
1
  import { MilkdownPlugin, Ctx } from '@milkdown/kit/ctx';
2
2
  import { KatexOptions } from 'katex';
3
- import { b as KunEditorLocale, N as Notify, U as UploadImage, K as KunEditorAdapters, a as KunEditorFeatures } from '../types-Dui0tGlh.cjs';
3
+ import { b as KunEditorLocale, N as Notify, U as UploadImage, K as KunEditorAdapters, a as KunEditorFeatures } from '../types-f8Szrgvw.cjs';
4
4
  import * as _milkdown_kit_utils from '@milkdown/kit/utils';
5
5
  import { Extension } from '@codemirror/state';
6
6
  import { Uploader } from '@milkdown/kit/plugin/upload';
@@ -13,8 +13,30 @@ declare const spoilerAttr: _milkdown_kit_utils.$NodeAttr;
13
13
  /** The `kun-spoiler` inline node: parses/serializes `||text||` and renders a
14
14
  * `<span data-type="kun-spoiler">` the render layer toggles on click. */
15
15
  declare const spoilerSchema: _milkdown_kit_utils.$NodeSchema<"kun-spoiler">;
16
- /** Toolbar entry point: wraps the current selection in a spoiler node. The
17
- * render layer's toolbar calls this via commandsCtx (P3). */
16
+ /**
17
+ * Toolbar entry point ("隐藏文本"): hide the selection, or reveal the spoiler the
18
+ * caret is in. The only way into a spoiler other than typing `||…||`, so it has
19
+ * to cover the same cases the syntax does:
20
+ *
21
+ * - a selection → its TEXT moves INSIDE the node. It has to be moved, not
22
+ * replaced: `replaceSelectionWith` on its own deletes what you selected. The
23
+ * schema is `marks: ''` (bold inside `||…||` cannot round-trip), so the
24
+ * formatting is what's dropped, never the words.
25
+ * - an empty selection → an empty spoiler with the caret inside it: type, and
26
+ * what you type is hidden. "Empty" means holding a caret anchor — a caret
27
+ * cannot sit in an inline node with no text node in it — and a spoiler with
28
+ * nothing but an anchor serializes to nothing at all, so an unused one cannot
29
+ * rot into the document.
30
+ * - the caret already inside a spoiler → reveal it, leaving the text selected.
31
+ * Nesting would serialize to `||||text||||`, which reads back as nothing at
32
+ * all, so this button has to toggle.
33
+ *
34
+ * Returns false where there is nothing to hide or nowhere to put it — inside a
35
+ * code block, or with a NODE selected (an image is not text; replacing it would
36
+ * delete it) — so a host can grey the button out. A spoiler holds text, so any
37
+ * other inline node caught in a text selection (an image, inline math) does not
38
+ * survive the move: `||…||` cannot carry one.
39
+ */
18
40
  declare const insertKunSpoilerCommand: _milkdown_kit_utils.$Command<unknown>;
19
41
  /** Typing `||text||` in the editor turns it into a spoiler node in place. The
20
42
  * trailing zero-width space gives the caret a stable anchor after the atom. */
@@ -39,7 +61,11 @@ declare const linkCustomKeymap: _milkdown_kit_utils.$UserKeymap<"linkCustomKeyma
39
61
  declare const createStopLinkPlugin: () => MilkdownPlugin[];
40
62
 
41
63
  interface InsertLinkPayload {
42
- /** The URL. Required — a blank href is a no-op. */
64
+ /**
65
+ * The URL. Required — a blank href is a no-op. Normalized on the way in, so a
66
+ * schemeless host gets one: `www.a.com/x` → `https://www.a.com/x`. See
67
+ * {@link normalizeLinkHref} for the exact rules.
68
+ */
43
69
  href: string;
44
70
  /** Text to insert + link when the selection is empty. Defaults to the href. */
45
71
  text?: string;
@@ -48,6 +74,10 @@ interface InsertLinkPayload {
48
74
  * Apply the `link` mark with `href` to the selection; on an EMPTY selection,
49
75
  * insert `text` (or the href) and link that. Clears the stored mark afterwards so
50
76
  * typing past the link isn't linked (same intent as the stop-link keymap).
77
+ *
78
+ * The href is normalized here — this command is the single funnel every link
79
+ * entry point (bubble, toolbar, host `linkPrompt`) goes through, so a UI never
80
+ * has to, and every one of them behaves the same.
51
81
  */
52
82
  declare const insertLinkCommand: _milkdown_kit_utils.$Command<InsertLinkPayload>;
53
83
  /** The link plugin bundle: the insert-link command. */
@@ -1,6 +1,6 @@
1
1
  import { MilkdownPlugin, Ctx } from '@milkdown/kit/ctx';
2
2
  import { KatexOptions } from 'katex';
3
- import { b as KunEditorLocale, N as Notify, U as UploadImage, K as KunEditorAdapters, a as KunEditorFeatures } from '../types-Dui0tGlh.js';
3
+ import { b as KunEditorLocale, N as Notify, U as UploadImage, K as KunEditorAdapters, a as KunEditorFeatures } from '../types-f8Szrgvw.js';
4
4
  import * as _milkdown_kit_utils from '@milkdown/kit/utils';
5
5
  import { Extension } from '@codemirror/state';
6
6
  import { Uploader } from '@milkdown/kit/plugin/upload';
@@ -13,8 +13,30 @@ declare const spoilerAttr: _milkdown_kit_utils.$NodeAttr;
13
13
  /** The `kun-spoiler` inline node: parses/serializes `||text||` and renders a
14
14
  * `<span data-type="kun-spoiler">` the render layer toggles on click. */
15
15
  declare const spoilerSchema: _milkdown_kit_utils.$NodeSchema<"kun-spoiler">;
16
- /** Toolbar entry point: wraps the current selection in a spoiler node. The
17
- * render layer's toolbar calls this via commandsCtx (P3). */
16
+ /**
17
+ * Toolbar entry point ("隐藏文本"): hide the selection, or reveal the spoiler the
18
+ * caret is in. The only way into a spoiler other than typing `||…||`, so it has
19
+ * to cover the same cases the syntax does:
20
+ *
21
+ * - a selection → its TEXT moves INSIDE the node. It has to be moved, not
22
+ * replaced: `replaceSelectionWith` on its own deletes what you selected. The
23
+ * schema is `marks: ''` (bold inside `||…||` cannot round-trip), so the
24
+ * formatting is what's dropped, never the words.
25
+ * - an empty selection → an empty spoiler with the caret inside it: type, and
26
+ * what you type is hidden. "Empty" means holding a caret anchor — a caret
27
+ * cannot sit in an inline node with no text node in it — and a spoiler with
28
+ * nothing but an anchor serializes to nothing at all, so an unused one cannot
29
+ * rot into the document.
30
+ * - the caret already inside a spoiler → reveal it, leaving the text selected.
31
+ * Nesting would serialize to `||||text||||`, which reads back as nothing at
32
+ * all, so this button has to toggle.
33
+ *
34
+ * Returns false where there is nothing to hide or nowhere to put it — inside a
35
+ * code block, or with a NODE selected (an image is not text; replacing it would
36
+ * delete it) — so a host can grey the button out. A spoiler holds text, so any
37
+ * other inline node caught in a text selection (an image, inline math) does not
38
+ * survive the move: `||…||` cannot carry one.
39
+ */
18
40
  declare const insertKunSpoilerCommand: _milkdown_kit_utils.$Command<unknown>;
19
41
  /** Typing `||text||` in the editor turns it into a spoiler node in place. The
20
42
  * trailing zero-width space gives the caret a stable anchor after the atom. */
@@ -39,7 +61,11 @@ declare const linkCustomKeymap: _milkdown_kit_utils.$UserKeymap<"linkCustomKeyma
39
61
  declare const createStopLinkPlugin: () => MilkdownPlugin[];
40
62
 
41
63
  interface InsertLinkPayload {
42
- /** The URL. Required — a blank href is a no-op. */
64
+ /**
65
+ * The URL. Required — a blank href is a no-op. Normalized on the way in, so a
66
+ * schemeless host gets one: `www.a.com/x` → `https://www.a.com/x`. See
67
+ * {@link normalizeLinkHref} for the exact rules.
68
+ */
43
69
  href: string;
44
70
  /** Text to insert + link when the selection is empty. Defaults to the href. */
45
71
  text?: string;
@@ -48,6 +74,10 @@ interface InsertLinkPayload {
48
74
  * Apply the `link` mark with `href` to the selection; on an EMPTY selection,
49
75
  * insert `text` (or the href) and link that. Clears the stored mark afterwards so
50
76
  * typing past the link isn't linked (same intent as the stop-link keymap).
77
+ *
78
+ * The href is normalized here — this command is the single funnel every link
79
+ * entry point (bubble, toolbar, host `linkPrompt`) goes through, so a UI never
80
+ * has to, and every one of them behaves the same.
51
81
  */
52
82
  declare const insertLinkCommand: _milkdown_kit_utils.$Command<InsertLinkPayload>;
53
83
  /** The link plugin bundle: the insert-link command. */
@@ -1,4 +1,4 @@
1
- import { QUOTE_SCHEME, MENTION_SCHEME } from '../chunk-PT42ZQGO.js';
1
+ import { normalizeLinkHref, QUOTE_SCHEME, MENTION_SCHEME } from '../chunk-IIVFLZTN.js';
2
2
  import { linkSchema, headingSchema, paragraphSchema, codeBlockSchema, commonmark } from '@milkdown/kit/preset/commonmark';
3
3
  import { gfm } from '@milkdown/kit/preset/gfm';
4
4
  import { history } from '@milkdown/kit/plugin/history';
@@ -6,12 +6,13 @@ import { listener } from '@milkdown/kit/plugin/listener';
6
6
  import { clipboard } from '@milkdown/kit/plugin/clipboard';
7
7
  import { indent } from '@milkdown/kit/plugin/indent';
8
8
  import { trailing } from '@milkdown/kit/plugin/trailing';
9
+ import { Fragment } from '@milkdown/kit/prose/model';
9
10
  import { expectDomTypeError } from '@milkdown/kit/exception';
10
11
  import { InputRule, textblockTypeInputRule } from '@milkdown/kit/prose/inputrules';
12
+ import { TextSelection, NodeSelection, PluginKey, Plugin } from '@milkdown/kit/prose/state';
11
13
  import { $nodeAttr, $nodeSchema, $command, $inputRule, $remark, $useKeymap, $prose } from '@milkdown/kit/utils';
12
14
  import { visit } from 'unist-util-visit';
13
15
  import { commandsCtx, editorViewOptionsCtx } from '@milkdown/kit/core';
14
- import { PluginKey, Plugin, NodeSelection, TextSelection } from '@milkdown/kit/prose/state';
15
16
  import { setBlockType } from '@milkdown/kit/prose/commands';
16
17
  import { findNodeInSelection, nodeRule } from '@milkdown/kit/prose';
17
18
  import katex from 'katex';
@@ -32,6 +33,21 @@ var spoilerAttr = $nodeAttr("kun-spoiler", () => ({
32
33
  style: "background: var(--color-default-500); border-radius: var(--radius-sm); padding: 0 4px; cursor: pointer;"
33
34
  }
34
35
  }));
36
+ var CARET_ANCHOR = "\u200B";
37
+ var withoutAnchors = (content) => {
38
+ const kept = [];
39
+ content.forEach((child) => {
40
+ if (!child.isText) {
41
+ kept.push(child);
42
+ return;
43
+ }
44
+ const text = (child.text ?? "").replaceAll(CARET_ANCHOR, "");
45
+ if (text) {
46
+ kept.push(child.type.schema.text(text, child.marks));
47
+ }
48
+ });
49
+ return Fragment.fromArray(kept);
50
+ };
35
51
  var spoilerSchema = $nodeSchema("kun-spoiler", (ctx) => ({
36
52
  group: "inline",
37
53
  inline: true,
@@ -76,23 +92,76 @@ var spoilerSchema = $nodeSchema("kun-spoiler", (ctx) => ({
76
92
  toMarkdown: {
77
93
  match: (node) => node.type.name === "kun-spoiler",
78
94
  runner: (state, node) => {
95
+ const content = withoutAnchors(node.content);
96
+ if (!content.size) {
97
+ return;
98
+ }
79
99
  state.addNode("text", void 0, "||");
80
- state.next(node.content);
100
+ state.next(content);
81
101
  state.addNode("text", void 0, "||");
82
102
  }
83
103
  }
84
104
  }));
105
+ var enclosingSpoiler = ($pos, type) => {
106
+ for (let depth = $pos.depth; depth > 0; depth--) {
107
+ const node = $pos.node(depth);
108
+ if (node.type === type) {
109
+ return { pos: $pos.before(depth), node };
110
+ }
111
+ }
112
+ return null;
113
+ };
85
114
  var insertKunSpoilerCommand = $command(
86
115
  "InsertKunSpoiler",
87
116
  (ctx) => () => (state, dispatch) => {
88
- if (!dispatch) {
117
+ const type = spoilerSchema.type(ctx);
118
+ const { $from, from, to, empty } = state.selection;
119
+ const open = enclosingSpoiler($from, type);
120
+ if (open) {
121
+ if (dispatch) {
122
+ const { pos, node: node2 } = open;
123
+ const revealed = withoutAnchors(node2.content);
124
+ const end = pos + node2.nodeSize;
125
+ const trailing2 = state.doc.textBetween(
126
+ end,
127
+ Math.min(end + CARET_ANCHOR.length, state.doc.content.size)
128
+ );
129
+ const tr2 = state.tr.replaceWith(
130
+ pos,
131
+ trailing2 === CARET_ANCHOR ? end + CARET_ANCHOR.length : end,
132
+ revealed
133
+ );
134
+ if (revealed.size) {
135
+ tr2.setSelection(TextSelection.create(tr2.doc, pos, pos + revealed.size));
136
+ }
137
+ dispatch(tr2.scrollIntoView());
138
+ }
89
139
  return true;
90
140
  }
91
- const node = spoilerSchema.type(ctx).create();
92
- if (!node) {
141
+ if (state.selection instanceof NodeSelection) {
142
+ return false;
143
+ }
144
+ if ($from.parent.isTextblock && !$from.parent.canReplaceWith($from.index(), $from.index(), type)) {
145
+ return false;
146
+ }
147
+ if (!dispatch) {
93
148
  return true;
94
149
  }
95
- dispatch(state.tr.replaceSelectionWith(node).scrollIntoView());
150
+ const text = state.doc.textBetween(from, to, " ").replaceAll(CARET_ANCHOR, "");
151
+ const node = type.create(null, state.schema.text(text || CARET_ANCHOR));
152
+ const tr = state.tr.replaceSelectionWith(node, false);
153
+ const after = tr.selection.to;
154
+ tr.insertText(CARET_ANCHOR, after);
155
+ dispatch(
156
+ tr.setSelection(
157
+ TextSelection.create(
158
+ tr.doc,
159
+ // On the anchor inside the empty node (type and it is hidden), or
160
+ // past the one after a wrap (type and it is not).
161
+ empty ? after - node.nodeSize + 1 + CARET_ANCHOR.length : after + 1
162
+ )
163
+ ).scrollIntoView()
164
+ );
96
165
  return true;
97
166
  }
98
167
  );
@@ -109,7 +178,7 @@ var insertSpoilerInputRule = $inputRule(
109
178
  { revealed: false },
110
179
  schema.text(content)
111
180
  );
112
- const zeroWidthSpace = schema.text("\u200B");
181
+ const zeroWidthSpace = schema.text(CARET_ANCHOR);
113
182
  return tr.replaceWith(startPos, end, [spoilerNode, zeroWidthSpace]).setStoredMarks([]).scrollIntoView();
114
183
  })
115
184
  );
@@ -214,7 +283,7 @@ var createStopLinkPlugin = () => [stopLinkCommand, linkCustomKeymap, clearOrphan
214
283
  var insertLinkCommand = $command(
215
284
  "InsertKunLink",
216
285
  (ctx) => (payload) => (state, dispatch) => {
217
- const href = payload?.href?.trim();
286
+ const href = normalizeLinkHref(payload?.href ?? "");
218
287
  if (!href) {
219
288
  return false;
220
289
  }