@kungal/editor-core 0.35.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.
@@ -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. */
@@ -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. */
@@ -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
  );