@kungal/editor-core 0.36.0 → 0.38.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.
@@ -7,8 +7,27 @@ import { Uploader } from '@milkdown/kit/plugin/upload';
7
7
  import { Decoration, EditorView } from '@milkdown/kit/prose/view';
8
8
  import { HighlightStyle } from '@codemirror/language';
9
9
 
10
- /** DOM attrs for the rendered spoiler chip. The styling references KunUI CSS
11
- * variables so it inherits the host theme (the render layer ships the vars). */
10
+ /**
11
+ * DOM attrs for the spoiler chip AS SEEN WHILE EDITING a tinted chip, so you
12
+ * can tell hidden text from ordinary text while you write it. (The published
13
+ * page renders `||…||` itself; this styling never leaves the editor.)
14
+ *
15
+ * Self-sufficient on purpose: it used to be `background: var(--color-default-500)`
16
+ * with no fallback, so in any host that does not define that palette token — the
17
+ * headless editor being exactly such a host — the whole declaration was invalid
18
+ * and the chip computed to `rgba(0, 0, 0, 0)`. Hidden text looked like plain text.
19
+ *
20
+ * `--kun-spoiler-bg` is the theming hook: an INLINE style beats every selector a
21
+ * host stylesheet can write, so without a custom property the chip would be
22
+ * untouchable from CSS. Setting the property is not (a rule may set it on
23
+ * `.kun-spoiler` freely) — which is how @kungal/editor-nuxt paints it in KunUI
24
+ * colors, and how any host re-tints it in one line.
25
+ *
26
+ * The default is `currentColor`-derived rather than a fixed grey so it works on a
27
+ * light and a dark background alike; the plain rgba before it is what a browser
28
+ * without `color-mix()` keeps (an unknown function is dropped at parse time, so
29
+ * the earlier declaration survives — unlike a bad `var()`, which kills both).
30
+ */
12
31
  declare const spoilerAttr: _milkdown_kit_utils.$NodeAttr;
13
32
  /** The `kun-spoiler` inline node: parses/serializes `||text||` and renders a
14
33
  * `<span data-type="kun-spoiler">` the render layer toggles on click. */
@@ -21,7 +40,9 @@ declare const spoilerSchema: _milkdown_kit_utils.$NodeSchema<"kun-spoiler">;
21
40
  * - a selection → its TEXT moves INSIDE the node. It has to be moved, not
22
41
  * replaced: `replaceSelectionWith` on its own deletes what you selected. The
23
42
  * schema is `marks: ''` (bold inside `||…||` cannot round-trip), so the
24
- * formatting is what's dropped, never the words.
43
+ * formatting is what's dropped, never the words. Line breaks become spaces:
44
+ * `||…||` pairs within one line in every reader of the syntax, so a spoiler
45
+ * spanning lines would serialize to markdown that reads back as literal `||`.
25
46
  * - an empty selection → an empty spoiler with the caret inside it: type, and
26
47
  * what you type is hidden. "Empty" means holding a caret anchor — a caret
27
48
  * cannot sit in an inline node with no text node in it — and a spoiler with
@@ -32,10 +53,11 @@ declare const spoilerSchema: _milkdown_kit_utils.$NodeSchema<"kun-spoiler">;
32
53
  * all, so this button has to toggle.
33
54
  *
34
55
  * 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.
56
+ * code block, with a NODE selected, or over a selection holding no words at all
57
+ * (an image is not text; replacing it would delete it) — so a host can grey the
58
+ * button out. A spoiler holds text, so any other inline node caught in a text
59
+ * selection alongside words (an image, inline math) does not survive the move:
60
+ * `||…||` cannot carry one.
39
61
  */
40
62
  declare const insertKunSpoilerCommand: _milkdown_kit_utils.$Command<unknown>;
41
63
  /** Typing `||text||` in the editor turns it into a spoiler node in place. The
@@ -46,7 +68,8 @@ declare const insertSpoilerInputRule: _milkdown_kit_utils.$InputRule;
46
68
  declare const remarkSpoilerPlugin: _milkdown_kit_utils.$Remark<"remarkSpoiler", unknown>;
47
69
  /**
48
70
  * The spoiler plugin bundle: schema attrs, node schema, the `||…||` input rule,
49
- * the insert command and the remark round-trip. Pure no adapter needed.
71
+ * the insert command, the remark round-trip and the serializer guard that keeps
72
+ * caret anchors out of the markdown. Pure — no adapter needed.
50
73
  */
51
74
  declare const createSpoilerPlugin: () => MilkdownPlugin[];
52
75
 
@@ -7,8 +7,27 @@ import { Uploader } from '@milkdown/kit/plugin/upload';
7
7
  import { Decoration, EditorView } from '@milkdown/kit/prose/view';
8
8
  import { HighlightStyle } from '@codemirror/language';
9
9
 
10
- /** DOM attrs for the rendered spoiler chip. The styling references KunUI CSS
11
- * variables so it inherits the host theme (the render layer ships the vars). */
10
+ /**
11
+ * DOM attrs for the spoiler chip AS SEEN WHILE EDITING a tinted chip, so you
12
+ * can tell hidden text from ordinary text while you write it. (The published
13
+ * page renders `||…||` itself; this styling never leaves the editor.)
14
+ *
15
+ * Self-sufficient on purpose: it used to be `background: var(--color-default-500)`
16
+ * with no fallback, so in any host that does not define that palette token — the
17
+ * headless editor being exactly such a host — the whole declaration was invalid
18
+ * and the chip computed to `rgba(0, 0, 0, 0)`. Hidden text looked like plain text.
19
+ *
20
+ * `--kun-spoiler-bg` is the theming hook: an INLINE style beats every selector a
21
+ * host stylesheet can write, so without a custom property the chip would be
22
+ * untouchable from CSS. Setting the property is not (a rule may set it on
23
+ * `.kun-spoiler` freely) — which is how @kungal/editor-nuxt paints it in KunUI
24
+ * colors, and how any host re-tints it in one line.
25
+ *
26
+ * The default is `currentColor`-derived rather than a fixed grey so it works on a
27
+ * light and a dark background alike; the plain rgba before it is what a browser
28
+ * without `color-mix()` keeps (an unknown function is dropped at parse time, so
29
+ * the earlier declaration survives — unlike a bad `var()`, which kills both).
30
+ */
12
31
  declare const spoilerAttr: _milkdown_kit_utils.$NodeAttr;
13
32
  /** The `kun-spoiler` inline node: parses/serializes `||text||` and renders a
14
33
  * `<span data-type="kun-spoiler">` the render layer toggles on click. */
@@ -21,7 +40,9 @@ declare const spoilerSchema: _milkdown_kit_utils.$NodeSchema<"kun-spoiler">;
21
40
  * - a selection → its TEXT moves INSIDE the node. It has to be moved, not
22
41
  * replaced: `replaceSelectionWith` on its own deletes what you selected. The
23
42
  * schema is `marks: ''` (bold inside `||…||` cannot round-trip), so the
24
- * formatting is what's dropped, never the words.
43
+ * formatting is what's dropped, never the words. Line breaks become spaces:
44
+ * `||…||` pairs within one line in every reader of the syntax, so a spoiler
45
+ * spanning lines would serialize to markdown that reads back as literal `||`.
25
46
  * - an empty selection → an empty spoiler with the caret inside it: type, and
26
47
  * what you type is hidden. "Empty" means holding a caret anchor — a caret
27
48
  * cannot sit in an inline node with no text node in it — and a spoiler with
@@ -32,10 +53,11 @@ declare const spoilerSchema: _milkdown_kit_utils.$NodeSchema<"kun-spoiler">;
32
53
  * all, so this button has to toggle.
33
54
  *
34
55
  * 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.
56
+ * code block, with a NODE selected, or over a selection holding no words at all
57
+ * (an image is not text; replacing it would delete it) — so a host can grey the
58
+ * button out. A spoiler holds text, so any other inline node caught in a text
59
+ * selection alongside words (an image, inline math) does not survive the move:
60
+ * `||…||` cannot carry one.
39
61
  */
40
62
  declare const insertKunSpoilerCommand: _milkdown_kit_utils.$Command<unknown>;
41
63
  /** Typing `||text||` in the editor turns it into a spoiler node in place. The
@@ -46,7 +68,8 @@ declare const insertSpoilerInputRule: _milkdown_kit_utils.$InputRule;
46
68
  declare const remarkSpoilerPlugin: _milkdown_kit_utils.$Remark<"remarkSpoiler", unknown>;
47
69
  /**
48
70
  * The spoiler plugin bundle: schema attrs, node schema, the `||…||` input rule,
49
- * the insert command and the remark round-trip. Pure no adapter needed.
71
+ * the insert command, the remark round-trip and the serializer guard that keeps
72
+ * caret anchors out of the markdown. Pure — no adapter needed.
50
73
  */
51
74
  declare const createSpoilerPlugin: () => MilkdownPlugin[];
52
75
 
@@ -6,13 +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 { commandsCtx, editorViewOptionsCtx, SerializerReady, serializerCtx } from '@milkdown/kit/core';
9
10
  import { Fragment } from '@milkdown/kit/prose/model';
10
11
  import { expectDomTypeError } from '@milkdown/kit/exception';
11
12
  import { InputRule, textblockTypeInputRule } from '@milkdown/kit/prose/inputrules';
12
13
  import { TextSelection, NodeSelection, PluginKey, Plugin } from '@milkdown/kit/prose/state';
13
14
  import { $nodeAttr, $nodeSchema, $command, $inputRule, $remark, $useKeymap, $prose } from '@milkdown/kit/utils';
14
15
  import { visit } from 'unist-util-visit';
15
- import { commandsCtx, editorViewOptionsCtx } from '@milkdown/kit/core';
16
16
  import { setBlockType } from '@milkdown/kit/prose/commands';
17
17
  import { findNodeInSelection, nodeRule } from '@milkdown/kit/prose';
18
18
  import katex from 'katex';
@@ -30,10 +30,17 @@ import { DecorationSet, Decoration } from '@milkdown/kit/prose/view';
30
30
  var spoilerAttr = $nodeAttr("kun-spoiler", () => ({
31
31
  container: {
32
32
  class: "kun-spoiler",
33
- style: "background: var(--color-default-500); border-radius: var(--radius-sm); padding: 0 4px; cursor: pointer;"
33
+ style: "background: rgba(127, 127, 127, 0.3); background: var(--kun-spoiler-bg, color-mix(in oklab, currentColor 22%, transparent)); border-radius: var(--radius-sm, 0.25rem); padding: 0 4px; cursor: pointer;"
34
34
  }
35
35
  }));
36
36
  var CARET_ANCHOR = "\u200B";
37
+ var stripCaretAnchors = (ctx) => async () => {
38
+ await ctx.wait(SerializerReady);
39
+ ctx.update(
40
+ serializerCtx,
41
+ (serialize) => (doc) => serialize(doc).replaceAll(CARET_ANCHOR, "")
42
+ );
43
+ };
37
44
  var withoutAnchors = (content) => {
38
45
  const kept = [];
39
46
  content.forEach((child) => {
@@ -144,10 +151,13 @@ var insertKunSpoilerCommand = $command(
144
151
  if ($from.parent.isTextblock && !$from.parent.canReplaceWith($from.index(), $from.index(), type)) {
145
152
  return false;
146
153
  }
154
+ const text = state.doc.textBetween(from, to, " ", " ").replaceAll(CARET_ANCHOR, "");
155
+ if (!empty && !text.trim()) {
156
+ return false;
157
+ }
147
158
  if (!dispatch) {
148
159
  return true;
149
160
  }
150
- const text = state.doc.textBetween(from, to, " ").replaceAll(CARET_ANCHOR, "");
151
161
  const node = type.create(null, state.schema.text(text || CARET_ANCHOR));
152
162
  const tr = state.tr.replaceSelectionWith(node, false);
153
163
  const after = tr.selection.to;
@@ -191,16 +201,17 @@ var remarkSpoilerPlugin = $remark("remarkSpoiler", () => () => {
191
201
  if (!node.value.includes("||")) {
192
202
  return;
193
203
  }
204
+ const value = node.value.replaceAll(CARET_ANCHOR, "");
194
205
  const regex = /\|\|(.*?)\|\|/g;
195
206
  const newNodes = [];
196
207
  let lastIndex = 0;
197
- for (const match of node.value.matchAll(regex)) {
208
+ for (const match of value.matchAll(regex)) {
198
209
  const [full, content] = match;
199
210
  const matchIndex = match.index ?? 0;
200
211
  if (matchIndex > lastIndex) {
201
212
  newNodes.push({
202
213
  type: "text",
203
- value: node.value.slice(lastIndex, matchIndex)
214
+ value: value.slice(lastIndex, matchIndex)
204
215
  });
205
216
  }
206
217
  if (content) {
@@ -208,11 +219,12 @@ var remarkSpoilerPlugin = $remark("remarkSpoiler", () => () => {
208
219
  type: "kun-spoiler",
209
220
  children: [{ type: "text", value: content }]
210
221
  });
222
+ newNodes.push({ type: "text", value: CARET_ANCHOR });
211
223
  }
212
224
  lastIndex = matchIndex + full.length;
213
225
  }
214
- if (lastIndex < node.value.length) {
215
- newNodes.push({ type: "text", value: node.value.slice(lastIndex) });
226
+ if (lastIndex < value.length) {
227
+ newNodes.push({ type: "text", value: value.slice(lastIndex) });
216
228
  }
217
229
  if (newNodes.length > 0 && typeof index === "number") {
218
230
  parent.children?.splice(index, 1, ...newNodes);
@@ -226,7 +238,8 @@ var createSpoilerPlugin = () => [
226
238
  spoilerSchema,
227
239
  insertSpoilerInputRule,
228
240
  insertKunSpoilerCommand,
229
- remarkSpoilerPlugin
241
+ remarkSpoilerPlugin,
242
+ stripCaretAnchors
230
243
  ].flat();
231
244
  var hasMark = (state, type) => {
232
245
  if (!type) {