@kungal/editor-core 0.35.0 → 0.37.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,33 @@ 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. Line breaks become spaces:
25
+ * `||…||` pairs within one line in every reader of the syntax, so a spoiler
26
+ * spanning lines would serialize to markdown that reads back as literal `||`.
27
+ * - an empty selection → an empty spoiler with the caret inside it: type, and
28
+ * what you type is hidden. "Empty" means holding a caret anchor — a caret
29
+ * cannot sit in an inline node with no text node in it — and a spoiler with
30
+ * nothing but an anchor serializes to nothing at all, so an unused one cannot
31
+ * rot into the document.
32
+ * - the caret already inside a spoiler → reveal it, leaving the text selected.
33
+ * Nesting would serialize to `||||text||||`, which reads back as nothing at
34
+ * all, so this button has to toggle.
35
+ *
36
+ * Returns false where there is nothing to hide or nowhere to put it — inside a
37
+ * code block, with a NODE selected, or over a selection holding no words at all
38
+ * (an image is not text; replacing it would delete it) — so a host can grey the
39
+ * button out. A spoiler holds text, so any other inline node caught in a text
40
+ * selection alongside words (an image, inline math) does not survive the move:
41
+ * `||…||` cannot carry one.
42
+ */
18
43
  declare const insertKunSpoilerCommand: _milkdown_kit_utils.$Command<unknown>;
19
44
  /** Typing `||text||` in the editor turns it into a spoiler node in place. The
20
45
  * trailing zero-width space gives the caret a stable anchor after the atom. */
@@ -13,8 +13,33 @@ 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. Line breaks become spaces:
25
+ * `||…||` pairs within one line in every reader of the syntax, so a spoiler
26
+ * spanning lines would serialize to markdown that reads back as literal `||`.
27
+ * - an empty selection → an empty spoiler with the caret inside it: type, and
28
+ * what you type is hidden. "Empty" means holding a caret anchor — a caret
29
+ * cannot sit in an inline node with no text node in it — and a spoiler with
30
+ * nothing but an anchor serializes to nothing at all, so an unused one cannot
31
+ * rot into the document.
32
+ * - the caret already inside a spoiler → reveal it, leaving the text selected.
33
+ * Nesting would serialize to `||||text||||`, which reads back as nothing at
34
+ * all, so this button has to toggle.
35
+ *
36
+ * Returns false where there is nothing to hide or nowhere to put it — inside a
37
+ * code block, with a NODE selected, or over a selection holding no words at all
38
+ * (an image is not text; replacing it would delete it) — so a host can grey the
39
+ * button out. A spoiler holds text, so any other inline node caught in a text
40
+ * selection alongside words (an image, inline math) does not survive the move:
41
+ * `||…||` cannot carry one.
42
+ */
18
43
  declare const insertKunSpoilerCommand: _milkdown_kit_utils.$Command<unknown>;
19
44
  /** Typing `||text||` in the editor turns it into a spoiler node in place. The
20
45
  * 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,79 @@ 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
+ const text = state.doc.textBetween(from, to, " ", " ").replaceAll(CARET_ANCHOR, "");
148
+ if (!empty && !text.trim()) {
149
+ return false;
150
+ }
151
+ if (!dispatch) {
93
152
  return true;
94
153
  }
95
- dispatch(state.tr.replaceSelectionWith(node).scrollIntoView());
154
+ const node = type.create(null, state.schema.text(text || CARET_ANCHOR));
155
+ const tr = state.tr.replaceSelectionWith(node, false);
156
+ const after = tr.selection.to;
157
+ tr.insertText(CARET_ANCHOR, after);
158
+ dispatch(
159
+ tr.setSelection(
160
+ TextSelection.create(
161
+ tr.doc,
162
+ // On the anchor inside the empty node (type and it is hidden), or
163
+ // past the one after a wrap (type and it is not).
164
+ empty ? after - node.nodeSize + 1 + CARET_ANCHOR.length : after + 1
165
+ )
166
+ ).scrollIntoView()
167
+ );
96
168
  return true;
97
169
  }
98
170
  );
@@ -109,7 +181,7 @@ var insertSpoilerInputRule = $inputRule(
109
181
  { revealed: false },
110
182
  schema.text(content)
111
183
  );
112
- const zeroWidthSpace = schema.text("\u200B");
184
+ const zeroWidthSpace = schema.text(CARET_ANCHOR);
113
185
  return tr.replaceWith(startPos, end, [spoilerNode, zeroWidthSpace]).setStoredMarks([]).scrollIntoView();
114
186
  })
115
187
  );