@meowdown/core 0.64.0 → 0.64.1

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 (3) hide show
  1. package/dist/index.d.ts +320 -113
  2. package/dist/index.js +511 -278
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -83,6 +83,28 @@ function hasPointerSelectionTransaction(transactions) {
83
83
  return transactions.some(isPointerSelectionTransaction);
84
84
  }
85
85
 
86
+ //#endregion
87
+ //#region src/extensions/mark-names.ts
88
+ function isMarkOfType(mark, name) {
89
+ return mark.type.name === name;
90
+ }
91
+ const SYNTAX_MARK_NAMES = /* @__PURE__ */ new Set([
92
+ "mdMark",
93
+ "mdLinkUri",
94
+ "mdLinkTitle"
95
+ ]);
96
+ const ATOM_MARK_NAMES = /* @__PURE__ */ new Set([
97
+ "mdWikilink",
98
+ "mdImage",
99
+ "mdFile",
100
+ "mdMath"
101
+ ]);
102
+ const ATOM_PACK_KEYS = /* @__PURE__ */ new Set([
103
+ "wikilink",
104
+ "image",
105
+ "file"
106
+ ]);
107
+
86
108
  //#endregion
87
109
  //#region src/extensions/mark-mode.ts
88
110
  const markModeKey = new PluginKey("mark-mode");
@@ -133,8 +155,8 @@ function getMarkMode(state) {
133
155
  * `getMarkRange` finds the unit, returning the outermost when units nest. One
134
156
  * decoration over its range flips the hidden punctuation/url/source visible via
135
157
  * the `.show` CSS rule. Because the range covers the whole unit, a caret at
136
- * either edge (e.g. right after a link's `)`) still reveals it. Wikilink and
137
- * `#tag` carry no `mdPack`, so they never reveal.
158
+ * either edge (e.g. right after a link's `)`) still reveals it. Atom packs
159
+ * (`ATOM_PACK_KEYS`) and `#tag` never reveal.
138
160
  */
139
161
  function computeFocusDecorations(state) {
140
162
  return computeRevealDecorations(state, void 0);
@@ -154,10 +176,164 @@ function computeRevealDecorations(state, packAttrs) {
154
176
  const $pos = selection.$head;
155
177
  const { parent } = $pos;
156
178
  if (!parent.isTextblock || parent.type.spec.code) return DecorationSet.empty;
157
- const range = getMarkRange($pos, getMarkType(state.schema, "mdPack"), packAttrs);
179
+ const range = getRevealablePackRange(state, $pos, packAttrs);
158
180
  if (!range) return DecorationSet.empty;
159
181
  return DecorationSet.create(state.doc, [Decoration.inline(range.from, range.to, { class: "show" })]);
160
182
  }
183
+ function isAtomPack(mark) {
184
+ return ATOM_PACK_KEYS.has(mark.attrs.key);
185
+ }
186
+ function getRevealablePackRange(state, $pos, packAttrs) {
187
+ const packType = getMarkType(state.schema, "mdPack");
188
+ const range = getMarkRange($pos, packType, packAttrs);
189
+ if (!range) return;
190
+ if (!isAtomPack(range.mark)) return range;
191
+ if ($pos.parentOffset === 0) return;
192
+ const before = getMarkRange(state.doc.resolve($pos.pos - 1), packType, packAttrs);
193
+ if (!before || before.to !== $pos.pos || isAtomPack(before.mark)) return;
194
+ return before;
195
+ }
196
+
197
+ //#endregion
198
+ //#region src/extensions/hidden-run.ts
199
+ function getCharMarks(state, pos) {
200
+ if (pos < 0 || pos + 1 > state.doc.content.size) return;
201
+ const $pos = state.doc.resolve(pos);
202
+ const child = $pos.parent.maybeChild($pos.index());
203
+ if (child == null || !child.isText) return;
204
+ return child.marks;
205
+ }
206
+ function isHiddenChar(state, pos) {
207
+ const marks = getCharMarks(state, pos);
208
+ if (marks == null) return false;
209
+ return marks.some((mark) => SYNTAX_MARK_NAMES.has(mark.type.name));
210
+ }
211
+ function isInsideNonCodeTextblock(state, pos) {
212
+ if (pos < 0 || pos > state.doc.content.size) return false;
213
+ const $pos = state.doc.resolve(pos);
214
+ return $pos.parent.isTextblock && !$pos.parent.type.spec.code;
215
+ }
216
+ /**
217
+ * The maximal contiguous hidden run ending exactly at `pos`, or undefined.
218
+ */
219
+ function getHiddenRunBefore(state, pos) {
220
+ if (!isInsideNonCodeTextblock(state, pos)) return;
221
+ const blockStart = state.doc.resolve(pos).start();
222
+ let from = pos;
223
+ while (from > blockStart && isHiddenChar(state, from - 1)) from--;
224
+ return from < pos ? {
225
+ from,
226
+ to: pos
227
+ } : void 0;
228
+ }
229
+ /**
230
+ * The maximal contiguous hidden run starting exactly at `pos`, or undefined.
231
+ */
232
+ function getHiddenRunAfter(state, pos) {
233
+ if (!isInsideNonCodeTextblock(state, pos)) return;
234
+ const blockEnd = state.doc.resolve(pos).end();
235
+ let to = pos;
236
+ while (to < blockEnd && isHiddenChar(state, to)) to++;
237
+ return to > pos ? {
238
+ from: pos,
239
+ to
240
+ } : void 0;
241
+ }
242
+ function isHiddenRunInterior(state, pos) {
243
+ return isHiddenChar(state, pos - 1) && isHiddenChar(state, pos);
244
+ }
245
+ /**
246
+ * The full run around an interior position, or undefined for rest positions.
247
+ */
248
+ function getHiddenRunAround(state, pos) {
249
+ if (!isHiddenRunInterior(state, pos)) return;
250
+ const before = getHiddenRunBefore(state, pos);
251
+ if (!before) return;
252
+ const after = getHiddenRunAfter(state, pos);
253
+ if (!after) return;
254
+ return {
255
+ from: before.from,
256
+ to: after.to
257
+ };
258
+ }
259
+ function charHasMark(state, pos, mark) {
260
+ const marks = getCharMarks(state, pos);
261
+ return marks != null && mark.isInSet(marks);
262
+ }
263
+ function getInnermostPackRangeAt(state, charPos) {
264
+ const marks = getCharMarks(state, charPos);
265
+ if (marks == null) return;
266
+ const packType = getMarkType(state.schema, "mdPack");
267
+ const packs = marks.filter((mark) => mark.type === packType);
268
+ if (packs.length === 0) return;
269
+ const $pos = state.doc.resolve(charPos);
270
+ const blockStart = $pos.start();
271
+ const blockEnd = $pos.end();
272
+ let innermost;
273
+ for (const pack of packs) {
274
+ let from = charPos;
275
+ while (from > blockStart && charHasMark(state, from - 1, pack)) from--;
276
+ let to = charPos + 1;
277
+ while (to < blockEnd && charHasMark(state, to, pack)) to++;
278
+ if (innermost == null || to - from < innermost.to - innermost.from) innermost = {
279
+ from,
280
+ to
281
+ };
282
+ }
283
+ return innermost;
284
+ }
285
+ function isPackOuterEdge(state, run, edge) {
286
+ const pack = getInnermostPackRangeAt(state, edge === "from" ? run.from : run.to - 1);
287
+ if (pack == null) return false;
288
+ return edge === "from" ? pack.from === run.from : pack.to === run.to;
289
+ }
290
+ function getPointerEdge(state, run, pos) {
291
+ const fromIsOuter = isPackOuterEdge(state, run, "from");
292
+ const toIsOuter = isPackOuterEdge(state, run, "to");
293
+ if (fromIsOuter && !toIsOuter) return run.from;
294
+ if (toIsOuter && !fromIsOuter) return run.to;
295
+ return pos - run.from <= run.to - pos ? run.from : run.to;
296
+ }
297
+ /**
298
+ * The rest position for a caret that landed at `newPos`. `oldPos` supplies the
299
+ * travel direction for keyboard motion; `isPointer` selects the click rules.
300
+ */
301
+ function getRestPosition(state, oldPos, newPos, isPointer) {
302
+ if (!isInsideNonCodeTextblock(state, newPos)) return newPos;
303
+ const run = getHiddenRunAround(state, newPos);
304
+ if (run != null) {
305
+ if (!isPointer) return newPos >= oldPos ? run.to : run.from;
306
+ return getPointerEdge(state, run, newPos);
307
+ }
308
+ if (!isPointer) return newPos;
309
+ const runBefore = getHiddenRunBefore(state, newPos);
310
+ if (runBefore != null && isPackOuterEdge(state, runBefore, "from")) return runBefore.from;
311
+ const runAfter = getHiddenRunAfter(state, newPos);
312
+ if (runAfter != null && isPackOuterEdge(state, runAfter, "to")) return runAfter.to;
313
+ return newPos;
314
+ }
315
+ function getCaretTail(state, pos) {
316
+ if (!isInsideNonCodeTextblock(state, pos)) return;
317
+ const hiddenBefore = isHiddenChar(state, pos - 1);
318
+ const hiddenAfter = isHiddenChar(state, pos);
319
+ if (hiddenBefore === hiddenAfter) return;
320
+ return hiddenAfter ? "left" : "right";
321
+ }
322
+ /**
323
+ * The leading and trailing hidden runs of the innermost unit whose marker
324
+ * character sits at `charPos`, trailing first so callers can delete them in
325
+ * order without remapping. A fully hidden unit yields one run.
326
+ */
327
+ function getUnitMarkerRuns(state, charPos) {
328
+ const pack = getInnermostPackRangeAt(state, charPos);
329
+ if (pack == null) return [];
330
+ const leading = getHiddenRunAfter(state, pack.from);
331
+ const trailing = getHiddenRunBefore(state, pack.to);
332
+ const runs = [];
333
+ if (trailing != null) runs.push(trailing);
334
+ if (leading != null && (trailing == null || leading.from !== trailing.from)) runs.push(leading);
335
+ return runs;
336
+ }
161
337
 
162
338
  //#endregion
163
339
  //#region src/extensions/mark-range.ts
@@ -185,10 +361,30 @@ function getMarkRangeAt(state, pos, markName, attrs) {
185
361
  const markNames = Array.isArray(markName) ? markName : [markName];
186
362
  for (const name of markNames) {
187
363
  const range = getMarkRange($pos, name, attrs);
188
- if (range) return range;
364
+ if (range) return ATOM_MARK_NAMES.has(name) ? clampToUnitRange(state, $pos, range) : range;
189
365
  }
190
366
  }
191
367
  /**
368
+ * Two identical adjacent units carry equal atom marks (only their packs differ,
369
+ * by `slot`), so `getMarkRange`'s eq-based expansion runs across both. Clamp
370
+ * the range to the matched child's own unit: the innermost pack covering its
371
+ * first character. A child without a pack (a document not produced by the
372
+ * inline parser) keeps the unclamped range.
373
+ */
374
+ function clampToUnitRange(state, $pos, range) {
375
+ const parent = $pos.parent;
376
+ const after = parent.childAfter($pos.parentOffset);
377
+ const matched = after.node && range.mark.isInSet(after.node.marks) ? after : parent.childBefore($pos.parentOffset);
378
+ if (!matched.node) return range;
379
+ const pack = getInnermostPackRangeAt(state, $pos.start() + matched.offset);
380
+ if (!pack) return range;
381
+ return {
382
+ from: Math.max(range.from, pack.from),
383
+ to: Math.min(range.to, pack.to),
384
+ mark: range.mark
385
+ };
386
+ }
387
+ /**
192
388
  * Returns the run ending exactly at `pos`, the one immediately to its left.
193
389
  * Probes from inside the left neighbour (`pos - 1`): probing `pos` itself
194
390
  * cannot see that run when another run starts exactly there, because
@@ -470,7 +666,9 @@ const highlightToMarkdown = (node, _parent, state, info) => {
470
666
  function isCheckboxInput(node) {
471
667
  return node.tagName === "input" && node.properties.type === "checkbox";
472
668
  }
473
- /** The first checkbox `<input>` before any nested list, if any. */
669
+ /**
670
+ * The first checkbox `<input>` before any nested list, if any.
671
+ */
474
672
  function findCheckbox(node) {
475
673
  for (const child of node.children) {
476
674
  if (child.type !== "element") continue;
@@ -518,7 +716,9 @@ function normalizeTaskItem(node) {
518
716
  children: [input, ...content]
519
717
  };
520
718
  }
521
- /** `li` handler that recognizes ProseMirror-style task items, then delegates. */
719
+ /**
720
+ * `li` handler that recognizes ProseMirror-style task items, then delegates.
721
+ */
522
722
  const taskAwareListItem = (state, element) => {
523
723
  return defaultHandlers.li(state, normalizeTaskItem(element) ?? element);
524
724
  };
@@ -567,7 +767,9 @@ function createProcessor() {
567
767
  }).freeze();
568
768
  }
569
769
  const getProcessor = once(createProcessor);
570
- /** Convert HTML into Markdown text. */
770
+ /**
771
+ * Convert HTML into Markdown text.
772
+ */
571
773
  function htmlToMarkdown(html) {
572
774
  return String(getProcessor().processSync(html));
573
775
  }
@@ -584,43 +786,36 @@ function parsePositiveInteger(raw) {
584
786
  return value != null && value > 0 ? value : void 0;
585
787
  }
586
788
 
587
- //#endregion
588
- //#region src/extensions/mark-names.ts
589
- function isMarkOfType(mark, name) {
590
- return mark.type.name === name;
591
- }
592
- const SYNTAX_MARK_NAMES = /* @__PURE__ */ new Set([
593
- "mdMark",
594
- "mdLinkUri",
595
- "mdLinkTitle"
596
- ]);
597
- const ATOM_MARK_NAMES = /* @__PURE__ */ new Set([
598
- "mdWikilink",
599
- "mdImage",
600
- "mdFile",
601
- "mdMath"
602
- ]);
603
-
604
789
  //#endregion
605
790
  //#region src/extensions/inline-runs.ts
606
791
  function findAtomMark(marks) {
607
792
  return marks.find((mark) => ATOM_MARK_NAMES.has(mark.type.name));
608
793
  }
794
+ function findOwnPackMark(marks) {
795
+ return marks.findLast((mark) => isMarkOfType(mark, "mdPack"));
796
+ }
609
797
  function hasSyntaxMark(marks) {
610
798
  return marks.some((mark) => SYNTAX_MARK_NAMES.has(mark.type.name));
611
799
  }
612
800
  /**
613
801
  * Group a textblock's text nodes into atom units and plain runs. A unit's
614
- * text nodes share one mark instance (the inline parser creates each unit
615
- * mark once), so instance identity splits adjacent same-attrs units.
802
+ * text nodes carry equal packs, while an identical neighbour's pack differs
803
+ * by `slot`, so pack equality splits adjacent same-attrs units.
616
804
  */
617
805
  function groupInlineRuns(textblock) {
618
806
  const runs = [];
807
+ let previousPack;
619
808
  textblock.forEach((child) => {
620
- if (!child.isText || !child.text) return;
809
+ if (!child.isText || !child.text) {
810
+ previousPack = void 0;
811
+ return;
812
+ }
621
813
  const atom = findAtomMark(child.marks);
814
+ const pack = findOwnPackMark(child.marks);
622
815
  const last = runs.at(-1);
623
- if (atom != null && last != null && last.atom === atom) {
816
+ const continuesUnit = atom != null && last?.atom != null && pack != null && previousPack != null && pack.eq(previousPack);
817
+ previousPack = pack;
818
+ if (continuesUnit && last != null) {
624
819
  last.text += child.text;
625
820
  last.children.push(child);
626
821
  return;
@@ -702,7 +897,9 @@ function syncOpenWrappers(open, next, out) {
702
897
  });
703
898
  }
704
899
  }
705
- /** A soft break (a literal `\n` in the source) renders as `<br>`. */
900
+ /**
901
+ * A soft break (a literal `\n` in the source) renders as `<br>`.
902
+ */
706
903
  function appendTextWithBreaks(parent, text) {
707
904
  const lines = text.split("\n");
708
905
  for (const [index, line] of lines.entries()) {
@@ -758,7 +955,9 @@ function defineHeadingWhitespace() {
758
955
  whitespace: "pre"
759
956
  });
760
957
  }
761
- /** The clipboard DOM of a heading: semantic inline content plus `data-md`. */
958
+ /**
959
+ * The clipboard DOM of a heading: semantic inline content plus `data-md`.
960
+ */
762
961
  function headingClipboardDOM(node) {
763
962
  const attrs = node.attrs;
764
963
  return semanticTextblockDOM(`h${attrs.level}`, node, {
@@ -766,7 +965,9 @@ function headingClipboardDOM(node) {
766
965
  "data-closing-hashes": attrs.closingHashes != null ? String(attrs.closingHashes) : void 0
767
966
  });
768
967
  }
769
- /** The clipboard parse rules restoring a heading's source text from `data-md`. */
968
+ /**
969
+ * The clipboard parse rules restoring a heading's source text from `data-md`.
970
+ */
770
971
  function headingFromDOM() {
771
972
  return [
772
973
  1,
@@ -839,11 +1040,15 @@ function defineMeowdownParagraphSpec() {
839
1040
  }
840
1041
  });
841
1042
  }
842
- /** The clipboard DOM of a paragraph: semantic inline content plus `data-md`. */
1043
+ /**
1044
+ * The clipboard DOM of a paragraph: semantic inline content plus `data-md`.
1045
+ */
843
1046
  function paragraphClipboardDOM(node) {
844
1047
  return semanticTextblockDOM("p", node);
845
1048
  }
846
- /** The clipboard parse rules restoring a paragraph's source text from `data-md`. */
1049
+ /**
1050
+ * The clipboard parse rules restoring a paragraph's source text from `data-md`.
1051
+ */
847
1052
  function paragraphFromDOM() {
848
1053
  return [createSourceTextRule("p", "paragraph")];
849
1054
  }
@@ -968,7 +1173,9 @@ function appendText(output, text) {
968
1173
  output.trailingNewlines = text.length - index;
969
1174
  }
970
1175
  }
971
- /** Recursive worker of {@link extractStyledPlainText}. */
1176
+ /**
1177
+ * Recursive worker of {@link extractStyledPlainText}.
1178
+ */
972
1179
  function appendNodeText(node, output, insideLine) {
973
1180
  if (node.nodeType === Node.TEXT_NODE) {
974
1181
  appendText(output, node.nodeValue ?? "");
@@ -1103,7 +1310,9 @@ function definePlainTextPaste() {
1103
1310
 
1104
1311
  //#endregion
1105
1312
  //#region src/utils/backticks.ts
1106
- /** Length of the longest run of `charCode` in `text`, at least `min`. */
1313
+ /**
1314
+ * Length of the longest run of `charCode` in `text`, at least `min`.
1315
+ */
1107
1316
  function longestCharRun(text, charCode, min = 0) {
1108
1317
  let longest = min;
1109
1318
  let run = 0;
@@ -1113,7 +1322,9 @@ function longestCharRun(text, charCode, min = 0) {
1113
1322
  } else run = 0;
1114
1323
  return longest;
1115
1324
  }
1116
- /** Length of the longest run of backticks in `text`, at least `min`. */
1325
+ /**
1326
+ * Length of the longest run of backticks in `text`, at least `min`.
1327
+ */
1117
1328
  function longestBacktickRun(text, min = 0) {
1118
1329
  return longestCharRun(text, 96, min);
1119
1330
  }
@@ -1157,7 +1368,9 @@ function emitFrontmatter(body, out) {
1157
1368
  out.write("---");
1158
1369
  out.closeBlock();
1159
1370
  }
1160
- /** Heading prefixes indexed by level (1..6). Index 0 is a sentinel. */
1371
+ /**
1372
+ * Heading prefixes indexed by level (1..6). Index 0 is a sentinel.
1373
+ */
1161
1374
  const HEADING_PREFIX = [
1162
1375
  "",
1163
1376
  "# ",
@@ -1227,7 +1440,9 @@ var MdOut = class {
1227
1440
  this.emitDeferredBlankLine();
1228
1441
  this.deferredBlankPrefix = this.linePrefix;
1229
1442
  }
1230
- /** End the current block; the next write gets a blank line before it. */
1443
+ /**
1444
+ * End the current block; the next write gets a blank line before it.
1445
+ */
1231
1446
  closeBlock() {
1232
1447
  if (this.atLineStart && this.pendingFirst !== null) {
1233
1448
  this.emitDeferredBlankLine();
@@ -1449,7 +1664,9 @@ function toIndentedCode(code) {
1449
1664
  for (let i = 0; i < lines.length; i++) if (lines[i] !== "") lines[i] = ` ${lines[i]}`;
1450
1665
  return lines.join("\n");
1451
1666
  }
1452
- /** Whether any content line would read as a closing `$$` fence. */
1667
+ /**
1668
+ * Whether any content line would read as a closing `$$` fence.
1669
+ */
1453
1670
  function hasDollarFenceLine(code) {
1454
1671
  return code.split("\n").some((line) => line.trim() === "$$");
1455
1672
  }
@@ -1614,7 +1831,9 @@ function definePlainTextSerializer() {
1614
1831
  } }
1615
1832
  }));
1616
1833
  }
1617
- /** Drop the inline text a hide-mode editor never shows. */
1834
+ /**
1835
+ * Drop the inline text a hide-mode editor never shows.
1836
+ */
1618
1837
  function stripHiddenInline(slice) {
1619
1838
  return new Slice(mapFragment(slice.content), slice.openStart, slice.openEnd);
1620
1839
  }
@@ -1999,141 +2218,6 @@ function executeCommand(view, command) {
1999
2218
  return command(view.state, view.dispatch, view);
2000
2219
  }
2001
2220
 
2002
- //#endregion
2003
- //#region src/extensions/hidden-run.ts
2004
- function getCharMarks(state, pos) {
2005
- if (pos < 0 || pos + 1 > state.doc.content.size) return;
2006
- const $pos = state.doc.resolve(pos);
2007
- const child = $pos.parent.maybeChild($pos.index());
2008
- if (child == null || !child.isText) return;
2009
- return child.marks;
2010
- }
2011
- function isHiddenChar(state, pos) {
2012
- const marks = getCharMarks(state, pos);
2013
- if (marks == null) return false;
2014
- return marks.some((mark) => SYNTAX_MARK_NAMES.has(mark.type.name));
2015
- }
2016
- function isInsideNonCodeTextblock(state, pos) {
2017
- if (pos < 0 || pos > state.doc.content.size) return false;
2018
- const $pos = state.doc.resolve(pos);
2019
- return $pos.parent.isTextblock && !$pos.parent.type.spec.code;
2020
- }
2021
- /** The maximal contiguous hidden run ending exactly at `pos`, or undefined. */
2022
- function getHiddenRunBefore(state, pos) {
2023
- if (!isInsideNonCodeTextblock(state, pos)) return;
2024
- const blockStart = state.doc.resolve(pos).start();
2025
- let from = pos;
2026
- while (from > blockStart && isHiddenChar(state, from - 1)) from--;
2027
- return from < pos ? {
2028
- from,
2029
- to: pos
2030
- } : void 0;
2031
- }
2032
- /** The maximal contiguous hidden run starting exactly at `pos`, or undefined. */
2033
- function getHiddenRunAfter(state, pos) {
2034
- if (!isInsideNonCodeTextblock(state, pos)) return;
2035
- const blockEnd = state.doc.resolve(pos).end();
2036
- let to = pos;
2037
- while (to < blockEnd && isHiddenChar(state, to)) to++;
2038
- return to > pos ? {
2039
- from: pos,
2040
- to
2041
- } : void 0;
2042
- }
2043
- function isHiddenRunInterior(state, pos) {
2044
- return isHiddenChar(state, pos - 1) && isHiddenChar(state, pos);
2045
- }
2046
- /** The full run around an interior position, or undefined for rest positions. */
2047
- function getHiddenRunAround(state, pos) {
2048
- if (!isHiddenRunInterior(state, pos)) return;
2049
- const before = getHiddenRunBefore(state, pos);
2050
- if (!before) return;
2051
- const after = getHiddenRunAfter(state, pos);
2052
- if (!after) return;
2053
- return {
2054
- from: before.from,
2055
- to: after.to
2056
- };
2057
- }
2058
- function charHasMark(state, pos, mark) {
2059
- const marks = getCharMarks(state, pos);
2060
- return marks != null && mark.isInSet(marks);
2061
- }
2062
- function getInnermostPackRangeAt(state, charPos) {
2063
- const marks = getCharMarks(state, charPos);
2064
- if (marks == null) return;
2065
- const packType = getMarkType(state.schema, "mdPack");
2066
- const packs = marks.filter((mark) => mark.type === packType);
2067
- if (packs.length === 0) return;
2068
- const $pos = state.doc.resolve(charPos);
2069
- const blockStart = $pos.start();
2070
- const blockEnd = $pos.end();
2071
- let innermost;
2072
- for (const pack of packs) {
2073
- let from = charPos;
2074
- while (from > blockStart && charHasMark(state, from - 1, pack)) from--;
2075
- let to = charPos + 1;
2076
- while (to < blockEnd && charHasMark(state, to, pack)) to++;
2077
- if (innermost == null || to - from < innermost.to - innermost.from) innermost = {
2078
- from,
2079
- to
2080
- };
2081
- }
2082
- return innermost;
2083
- }
2084
- function isPackOuterEdge(state, run, edge) {
2085
- const pack = getInnermostPackRangeAt(state, edge === "from" ? run.from : run.to - 1);
2086
- if (pack == null) return false;
2087
- return edge === "from" ? pack.from === run.from : pack.to === run.to;
2088
- }
2089
- function getPointerEdge(state, run, pos) {
2090
- const fromIsOuter = isPackOuterEdge(state, run, "from");
2091
- const toIsOuter = isPackOuterEdge(state, run, "to");
2092
- if (fromIsOuter && !toIsOuter) return run.from;
2093
- if (toIsOuter && !fromIsOuter) return run.to;
2094
- return pos - run.from <= run.to - pos ? run.from : run.to;
2095
- }
2096
- /**
2097
- * The rest position for a caret that landed at `newPos`. `oldPos` supplies the
2098
- * travel direction for keyboard motion; `isPointer` selects the click rules.
2099
- */
2100
- function getRestPosition(state, oldPos, newPos, isPointer) {
2101
- if (!isInsideNonCodeTextblock(state, newPos)) return newPos;
2102
- const run = getHiddenRunAround(state, newPos);
2103
- if (run != null) {
2104
- if (!isPointer) return newPos >= oldPos ? run.to : run.from;
2105
- return getPointerEdge(state, run, newPos);
2106
- }
2107
- if (!isPointer) return newPos;
2108
- const runBefore = getHiddenRunBefore(state, newPos);
2109
- if (runBefore != null && isPackOuterEdge(state, runBefore, "from")) return runBefore.from;
2110
- const runAfter = getHiddenRunAfter(state, newPos);
2111
- if (runAfter != null && isPackOuterEdge(state, runAfter, "to")) return runAfter.to;
2112
- return newPos;
2113
- }
2114
- function getCaretTail(state, pos) {
2115
- if (!isInsideNonCodeTextblock(state, pos)) return;
2116
- const hiddenBefore = isHiddenChar(state, pos - 1);
2117
- const hiddenAfter = isHiddenChar(state, pos);
2118
- if (hiddenBefore === hiddenAfter) return;
2119
- return hiddenAfter ? "left" : "right";
2120
- }
2121
- /**
2122
- * The leading and trailing hidden runs of the innermost unit whose marker
2123
- * character sits at `charPos`, trailing first so callers can delete them in
2124
- * order without remapping. A fully hidden unit yields one run.
2125
- */
2126
- function getUnitMarkerRuns(state, charPos) {
2127
- const pack = getInnermostPackRangeAt(state, charPos);
2128
- if (pack == null) return [];
2129
- const leading = getHiddenRunAfter(state, pack.from);
2130
- const trailing = getHiddenRunBefore(state, pack.to);
2131
- const runs = [];
2132
- if (trailing != null) runs.push(trailing);
2133
- if (leading != null && (trailing == null || leading.from !== trailing.from)) runs.push(leading);
2134
- return runs;
2135
- }
2136
-
2137
2221
  //#endregion
2138
2222
  //#region src/extensions/hidden-run-caret.ts
2139
2223
  const snapKey = new PluginKey("meowdown-hidden-run-snap");
@@ -2494,11 +2578,15 @@ function parseMagicComment(comment) {
2494
2578
  function toPositiveNumber(value) {
2495
2579
  if (typeof value === "number" && Number.isFinite(value) && value > 0) return Math.round(value);
2496
2580
  }
2497
- /** The canonical comment meowdown writes for the metadata. */
2581
+ /**
2582
+ * The canonical comment meowdown writes for the metadata.
2583
+ */
2498
2584
  function formatMagicComment(magic) {
2499
2585
  return `<!-- ${JSON.stringify(magic)} -->`;
2500
2586
  }
2501
- /** Drop a trailing magic comment from the source text. */
2587
+ /**
2588
+ * Drop a trailing magic comment from the source text.
2589
+ */
2502
2590
  function stripMagicComment(source) {
2503
2591
  return source.replace(TRAILING_MAGIC_COMMENT_RE, "");
2504
2592
  }
@@ -2619,7 +2707,9 @@ function positiveInteger(value) {
2619
2707
  const parsed = Number.parseInt(value, 10);
2620
2708
  return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
2621
2709
  }
2622
- /** Parse `![[target]]`, `![[target|alias]]`, `![[target|width]]`, or `![[target|widthxheight]]`. */
2710
+ /**
2711
+ * Parse `![[target]]`, `![[target|alias]]`, `![[target|width]]`, or `![[target|widthxheight]]`.
2712
+ */
2623
2713
  function parseWikiEmbed(source) {
2624
2714
  const inner = source.replace(/^!\[\[/, "").replace(/\]\]$/, "");
2625
2715
  const pipe = inner.lastIndexOf("|");
@@ -2653,11 +2743,15 @@ function parseWikiEmbed(source) {
2653
2743
  height
2654
2744
  };
2655
2745
  }
2656
- /** Rewrite a wiki image embed with a persisted display size. */
2746
+ /**
2747
+ * Rewrite a wiki image embed with a persisted display size.
2748
+ */
2657
2749
  function formatSizedWikiEmbed(target, width, height) {
2658
2750
  return `![[${target}|${Math.round(width)}x${Math.round(height)}]]`;
2659
2751
  }
2660
- /** Last path component of a target, with a note heading/block fragment removed. */
2752
+ /**
2753
+ * Last path component of a target, with a note heading/block fragment removed.
2754
+ */
2661
2755
  function wikiEmbedBasename(target) {
2662
2756
  const path = target.split(/[?#]/, 1)[0];
2663
2757
  const segment = path.split(/[/\\]/).findLast(Boolean) ?? path;
@@ -2670,7 +2764,9 @@ function wikiEmbedBasename(target) {
2670
2764
 
2671
2765
  //#endregion
2672
2766
  //#region src/extensions/wikilink.ts
2673
- /** Splits `[[target]]`/`[[target|alias]]` into its target and display label (the alias, or empty). */
2767
+ /**
2768
+ * Splits `[[target]]`/`[[target|alias]]` into its target and display label (the alias, or empty).
2769
+ */
2674
2770
  function parseWikilink(text) {
2675
2771
  const inner = text.replace(/^\[\[/, "").replace(/\]\]$/, "");
2676
2772
  const pipe = inner.indexOf("|");
@@ -2769,7 +2865,28 @@ function inlineTextToMarkChunksWithContext(marks, text, options, context) {
2769
2865
  walk(elements, [], 0, text.length, text, marks, out, options, context);
2770
2866
  return out;
2771
2867
  }
2772
- /** Drop the surrounding `"" '' ()` delimiters of a `LinkTitle` slice and unescape. */
2868
+ /**
2869
+ * The pack of one unit starting at `from`. When the chunk ending exactly there
2870
+ * closes a unit whose pack equals this one, create it with `slot: 1` instead:
2871
+ * equal packs would let ProseMirror merge the two units into one text node,
2872
+ * one mark run and one mark view. The neighbour's own pack sits at the same
2873
+ * depth, right after `parentMarks`; at any other depth that position holds a
2874
+ * different mark (or nothing) and never compares equal.
2875
+ */
2876
+ function createUnitPack(marks, out, parentMarks, from, attrs) {
2877
+ const pack = marks.mdPack.create(attrs);
2878
+ const previous = out.at(-1);
2879
+ if (previous == null || previous[1] !== from) return pack;
2880
+ const neighbourPack = previous[2][parentMarks.length];
2881
+ if (neighbourPack == null || !neighbourPack.eq(pack)) return pack;
2882
+ return marks.mdPack.create({
2883
+ ...attrs,
2884
+ slot: 1
2885
+ });
2886
+ }
2887
+ /**
2888
+ * Drop the surrounding `"" '' ()` delimiters of a `LinkTitle` slice and unescape.
2889
+ */
2773
2890
  function unquoteTitle(raw) {
2774
2891
  return raw.slice(1, -1).replaceAll(/\\(.)/g, "$1");
2775
2892
  }
@@ -2813,7 +2930,7 @@ function walkGenericNode(node, parentMarks, text, marks, out, options, context)
2813
2930
  else if (type === LEZER_NODE_IDS.Strikethrough) packKey = "strike";
2814
2931
  else if (type === LEZER_NODE_IDS.Highlight) packKey = "highlight";
2815
2932
  else if (type === LEZER_NODE_IDS.Autolink) packKey = "autolink";
2816
- const base = packKey ? [...parentMarks, marks.mdPack.create({ key: packKey })] : parentMarks;
2933
+ const base = packKey ? [...parentMarks, createUnitPack(marks, out, parentMarks, node.from, { key: packKey })] : parentMarks;
2817
2934
  const maybeMarkName = MARK_NAME_BY_TYPE_ID.get(type);
2818
2935
  const childMarks = maybeMarkName ? [...base, marks[maybeMarkName].create()] : base;
2819
2936
  if (node.children.length === 0) emit(out, node.from, node.to, childMarks);
@@ -2836,9 +2953,13 @@ function walkLink(node, parentMarks, text, marks, out, options, context) {
2836
2953
  walkUnresolvedLink(node, parentMarks, text, marks, out, options, context);
2837
2954
  return;
2838
2955
  }
2839
- const fileMarks = claimFileLink(parts, resolution, parentMarks, text, marks, options);
2840
- if (fileMarks) {
2841
- emit(out, node.from, node.to, fileMarks);
2956
+ const fileMark = claimFileLink(parts, resolution, text, marks, options);
2957
+ if (fileMark) {
2958
+ emit(out, node.from, node.to, [
2959
+ ...parentMarks,
2960
+ createUnitPack(marks, out, parentMarks, node.from, { key: "file" }),
2961
+ fileMark
2962
+ ]);
2842
2963
  return;
2843
2964
  }
2844
2965
  walkResolvedLink(node, parts, resolution, parentMarks, text, marks, out, options, context);
@@ -2902,7 +3023,9 @@ function walkUnresolvedLink(node, parentMarks, text, marks, out, options, contex
2902
3023
  return child.type !== LEZER_NODE_IDS.LinkMark && child.type !== LEZER_NODE_IDS.LinkLabel;
2903
3024
  }), parentMarks, node.from, node.to, text, marks, out, options, context);
2904
3025
  }
2905
- /** The last path segment of `href` (query/hash stripped), decoded when possible. */
3026
+ /**
3027
+ * The last path segment of `href` (query/hash stripped), decoded when possible.
3028
+ */
2906
3029
  function hrefBasename(href) {
2907
3030
  const path = href.split(/[?#]/, 1)[0];
2908
3031
  const segment = path.split(/[/\\]/).findLast(Boolean) ?? path;
@@ -2913,12 +3036,12 @@ function hrefBasename(href) {
2913
3036
  }
2914
3037
  }
2915
3038
  /**
2916
- * The marks for a whole inline or resolved reference link that the host's `resolveFileLink`
2917
- * claimed as a file, or `undefined` when the link stays a regular link. The
2918
- * resolver is never consulted for a link without a closed label or a non-empty
2919
- * destination.
3039
+ * The `mdFile` mark for a whole inline or resolved reference link that the
3040
+ * host's `resolveFileLink` claimed as a file, or `undefined` when the link
3041
+ * stays a regular link. The resolver is never consulted for a link without a
3042
+ * closed label or a non-empty destination.
2920
3043
  */
2921
- function claimFileLink(parts, resolution, parentMarks, text, marks, options) {
3044
+ function claimFileLink(parts, resolution, text, marks, options) {
2922
3045
  const resolveFileLink = options?.resolveFileLink;
2923
3046
  if (!resolveFileLink) return void 0;
2924
3047
  const { labelFrom, labelTo } = parts;
@@ -2932,11 +3055,11 @@ function claimFileLink(parts, resolution, parentMarks, text, marks, options) {
2932
3055
  title
2933
3056
  })) return void 0;
2934
3057
  const name = label || hrefBasename(href);
2935
- return [...parentMarks, marks.mdFile.create({
3058
+ return marks.mdFile.create({
2936
3059
  href,
2937
3060
  name,
2938
3061
  title
2939
- })];
3062
+ });
2940
3063
  }
2941
3064
  /**
2942
3065
  * Special walker for `Link` nodes.
@@ -2968,7 +3091,7 @@ function walkResolvedLink(node, parts, resolution, parentMarks, text, marks, out
2968
3091
  href,
2969
3092
  title
2970
3093
  };
2971
- const pack = marks.mdPack.create({
3094
+ const pack = createUnitPack(marks, out, parentMarks, node.from, {
2972
3095
  key: "link",
2973
3096
  data
2974
3097
  });
@@ -3038,15 +3161,19 @@ function walkImage(node, parentMarks, text, marks, out, options, context, traili
3038
3161
  const width = trailing?.magic.width ?? null;
3039
3162
  const height = trailing?.magic.height ?? null;
3040
3163
  const to = trailing?.to ?? node.to;
3041
- emit(out, node.from, to, [...parentMarks, marks.mdImage.create({
3042
- src,
3043
- alt,
3044
- title,
3045
- width,
3046
- height,
3047
- syntax: null,
3048
- wikiTarget: null
3049
- })]);
3164
+ emit(out, node.from, to, [
3165
+ ...parentMarks,
3166
+ createUnitPack(marks, out, parentMarks, node.from, { key: "image" }),
3167
+ marks.mdImage.create({
3168
+ src,
3169
+ alt,
3170
+ title,
3171
+ width,
3172
+ height,
3173
+ syntax: null,
3174
+ wikiTarget: null
3175
+ })
3176
+ ]);
3050
3177
  }
3051
3178
  /**
3052
3179
  * Special walker for inline math `$formula$`/`$$formula$$`.
@@ -3065,7 +3192,7 @@ function walkMath(node, parentMarks, text, marks, out) {
3065
3192
  const formula = text.slice(markNodes[0].to, markNodes[1].from);
3066
3193
  const base = [
3067
3194
  ...parentMarks,
3068
- marks.mdPack.create({ key: "math" }),
3195
+ createUnitPack(marks, out, parentMarks, node.from, { key: "math" }),
3069
3196
  marks.mdMath.create({ formula })
3070
3197
  ];
3071
3198
  emit(out, node.from, markNodes[0].to, [...base, marks.mdMark.create()]);
@@ -3077,10 +3204,14 @@ function walkMath(node, parentMarks, text, marks, out) {
3077
3204
  */
3078
3205
  function walkWikilink(node, parentMarks, text, marks, out) {
3079
3206
  const { target, display } = parseWikilink(text.slice(node.from, node.to));
3080
- emit(out, node.from, node.to, [...parentMarks, marks.mdWikilink.create({
3081
- target,
3082
- display
3083
- })]);
3207
+ emit(out, node.from, node.to, [
3208
+ ...parentMarks,
3209
+ createUnitPack(marks, out, parentMarks, node.from, { key: "wikilink" }),
3210
+ marks.mdWikilink.create({
3211
+ target,
3212
+ display
3213
+ })
3214
+ ]);
3084
3215
  }
3085
3216
  /**
3086
3217
  * Resolve `![[target]]` into one of Meowdown's existing source-backed atoms.
@@ -3097,33 +3228,45 @@ function walkWikiEmbed(node, parentMarks, text, marks, out, options) {
3097
3228
  if (resolution.kind === "image") {
3098
3229
  const src = resolution.src ?? embed.target;
3099
3230
  const alt = (resolution.alt ?? embed.display) || wikiEmbedBasename(embed.target);
3100
- emit(out, node.from, node.to, [...parentMarks, marks.mdImage.create({
3101
- src,
3102
- alt,
3103
- title: "",
3104
- width: embed.width,
3105
- height: embed.height,
3106
- syntax: "wikiEmbed",
3107
- wikiTarget: embed.target
3108
- })]);
3231
+ emit(out, node.from, node.to, [
3232
+ ...parentMarks,
3233
+ createUnitPack(marks, out, parentMarks, node.from, { key: "image" }),
3234
+ marks.mdImage.create({
3235
+ src,
3236
+ alt,
3237
+ title: "",
3238
+ width: embed.width,
3239
+ height: embed.height,
3240
+ syntax: "wikiEmbed",
3241
+ wikiTarget: embed.target
3242
+ })
3243
+ ]);
3109
3244
  return;
3110
3245
  }
3111
3246
  if (resolution.kind === "file") {
3112
3247
  const href = resolution.href ?? embed.target;
3113
3248
  const name = (resolution.name ?? embed.display) || wikiEmbedBasename(embed.target);
3114
- emit(out, node.from, node.to, [...parentMarks, marks.mdFile.create({
3115
- href,
3116
- name,
3117
- title: resolution.title ?? ""
3118
- })]);
3249
+ emit(out, node.from, node.to, [
3250
+ ...parentMarks,
3251
+ createUnitPack(marks, out, parentMarks, node.from, { key: "file" }),
3252
+ marks.mdFile.create({
3253
+ href,
3254
+ name,
3255
+ title: resolution.title ?? ""
3256
+ })
3257
+ ]);
3119
3258
  return;
3120
3259
  }
3121
3260
  const target = resolution.target ?? embed.target;
3122
3261
  const display = resolution.display ?? embed.display;
3123
- emit(out, node.from, node.to, [...parentMarks, marks.mdWikilink.create({
3124
- target,
3125
- display
3126
- })]);
3262
+ emit(out, node.from, node.to, [
3263
+ ...parentMarks,
3264
+ createUnitPack(marks, out, parentMarks, node.from, { key: "wikilink" }),
3265
+ marks.mdWikilink.create({
3266
+ target,
3267
+ display
3268
+ })
3269
+ ]);
3127
3270
  }
3128
3271
  /**
3129
3272
  * Push `[from, to, marks]` to `out`, coalescing with the previous chunk
@@ -3513,7 +3656,9 @@ function defineMdTag() {
3513
3656
  parseDOM: [{ tag: "span.md-tag" }]
3514
3657
  });
3515
3658
  }
3516
- /** Covers the whole `[[target]]`/`[[target|alias]]` source. */
3659
+ /**
3660
+ * Covers the whole `[[target]]`/`[[target|alias]]` source.
3661
+ */
3517
3662
  function defineMdWikilink() {
3518
3663
  return defineMarkSpec({
3519
3664
  name: "mdWikilink",
@@ -3547,7 +3692,9 @@ function defineMdFile() {
3547
3692
  parseDOM: [{ tag: "span.md-file" }]
3548
3693
  });
3549
3694
  }
3550
- /** Covers the whole `$formula$` source, dollars included. */
3695
+ /**
3696
+ * Covers the whole `$formula$` source, dollars included.
3697
+ */
3551
3698
  function defineMdMath() {
3552
3699
  return defineMarkSpec({
3553
3700
  name: "mdMath",
@@ -3562,9 +3709,10 @@ function defineMdMath() {
3562
3709
  });
3563
3710
  }
3564
3711
  /**
3565
- * Wraps a whole revealable inline unit (emphasis, strong, code, strikethrough,
3566
- * link, autolink, image) so focus mode can reveal the unit with one
3567
- * `getMarkRange` lookup instead of stitching its punctuation back together.
3712
+ * Wraps a whole inline unit. For a revealable unit (emphasis, strong, code,
3713
+ * strikethrough, link, autolink, math) focus mode reveals the unit with one
3714
+ * range lookup instead of stitching its punctuation back together; an atom
3715
+ * unit (wikilink, image, file) carries it purely as unit identity.
3568
3716
  * `excludes: ''` lets nested units carry two of these marks at once.
3569
3717
  */
3570
3718
  function defineMdPack() {
@@ -3574,7 +3722,8 @@ function defineMdPack() {
3574
3722
  inclusive: false,
3575
3723
  attrs: {
3576
3724
  key: {},
3577
- data: { default: null }
3725
+ data: { default: null },
3726
+ slot: { default: null }
3578
3727
  },
3579
3728
  toDOM: (mark) => {
3580
3729
  return [
@@ -3624,7 +3773,9 @@ const MARKER_IDS = /* @__PURE__ */ new Set([
3624
3773
  LEZER_NODE_IDS.StrikethroughMark,
3625
3774
  LEZER_NODE_IDS.HighlightMark
3626
3775
  ]);
3627
- /** The opening and closing delimiter tokens of a toggleable node. */
3776
+ /**
3777
+ * The opening and closing delimiter tokens of a toggleable node.
3778
+ */
3628
3779
  function delimiters(node) {
3629
3780
  return [node.children[0], node.children.at(-1)];
3630
3781
  }
@@ -3672,7 +3823,9 @@ function engulf(nodes, from, to) {
3672
3823
  }
3673
3824
  return [from, to];
3674
3825
  }
3675
- /** Shrink [from, to] so it starts and ends on non-whitespace. */
3826
+ /**
3827
+ * Shrink [from, to] so it starts and ends on non-whitespace.
3828
+ */
3676
3829
  function trimRange(text, from, to) {
3677
3830
  while (from < to && isSpaceChar(text.charCodeAt(from))) from++;
3678
3831
  while (to > from && isSpaceChar(text.charCodeAt(to - 1))) to--;
@@ -3832,7 +3985,9 @@ function caretPlan(text, pos, spec) {
3832
3985
  pos
3833
3986
  };
3834
3987
  }
3835
- /** Whether `pos` sits where inserted syntax could not parse: inside an atom or inside another span's delimiters. */
3988
+ /**
3989
+ * Whether `pos` sits where inserted syntax could not parse: inside an atom or inside another span's delimiters.
3990
+ */
3836
3991
  function insideAtom(nodes, pos) {
3837
3992
  for (const node of nodes) if (node.from < pos && pos < node.to) {
3838
3993
  const content = nestableContent(node);
@@ -4014,12 +4169,16 @@ function getLinkUnitAt(state, pos) {
4014
4169
 
4015
4170
  //#endregion
4016
4171
  //#region src/extensions/link-commands.ts
4017
- /** Normalize a typed URL with the existing autolink logic, else keep it verbatim. */
4172
+ /**
4173
+ * Normalize a typed URL with the existing autolink logic, else keep it verbatim.
4174
+ */
4018
4175
  function normalizeHref(raw) {
4019
4176
  const value = raw.trim();
4020
4177
  return value ? getAutolinkHref(value) ?? value : "";
4021
4178
  }
4022
- /** The `( ... )` body for a link: the href plus an optional CommonMark title. */
4179
+ /**
4180
+ * The `( ... )` body for a link: the href plus an optional CommonMark title.
4181
+ */
4023
4182
  function destText(href, title) {
4024
4183
  return href + (title ? ` "${title.replaceAll(/(["\\])/g, String.raw`\$1`)}"` : "");
4025
4184
  }
@@ -4059,7 +4218,9 @@ function insertLink({ href, title, wrapText = true } = {}) {
4059
4218
  return true;
4060
4219
  };
4061
4220
  }
4062
- /** Rewrite the `( ... )` of the link at the caret/selection. */
4221
+ /**
4222
+ * Rewrite the `( ... )` of the link at the caret/selection.
4223
+ */
4063
4224
  function updateLink(attrs) {
4064
4225
  return (state, dispatch) => {
4065
4226
  const link = getLinkUnitAt(state, state.selection.from);
@@ -4069,7 +4230,9 @@ function updateLink(attrs) {
4069
4230
  return true;
4070
4231
  };
4071
4232
  }
4072
- /** Unwrap the link at the caret: keep the label text, drop the syntax. */
4233
+ /**
4234
+ * Unwrap the link at the caret: keep the label text, drop the syntax.
4235
+ */
4073
4236
  function removeLink() {
4074
4237
  return (state, dispatch) => {
4075
4238
  const link = getLinkUnitAt(state, state.selection.from);
@@ -4290,26 +4453,38 @@ const listInputRules = [
4290
4453
  function defineMeowdownListInputRules() {
4291
4454
  return union(listInputRules.map(defineInputRule));
4292
4455
  }
4293
- /** Circle checkbox task: a `task` list item with a `+` marker. */
4456
+ /**
4457
+ * Circle checkbox task: a `task` list item with a `+` marker.
4458
+ */
4294
4459
  function wrapInCircleTask() {
4295
4460
  return wrapInList({
4296
4461
  kind: "task",
4297
4462
  marker: "+"
4298
4463
  });
4299
4464
  }
4300
- /** Square checkbox task: a `task` list item with the canonical `-` marker. */
4465
+ /**
4466
+ * Square checkbox task: a `task` list item with the canonical `-` marker.
4467
+ */
4301
4468
  function wrapInSquareTask() {
4302
4469
  return wrapInList({
4303
4470
  kind: "task",
4304
4471
  marker: null
4305
4472
  });
4306
4473
  }
4307
- /** The attributes of the closest list node enclosing the selection, if any. */
4474
+ /**
4475
+ * The attributes of the list item whose own content holds the selection, if
4476
+ * any. A continuation block deeper inside an item (a paragraph after the
4477
+ * item's first block) is not the item's content, so it reports no list: the
4478
+ * cycle commands then wrap it into a new nested list instead of reading the
4479
+ * ancestor's kind.
4480
+ */
4308
4481
  function getListAttrsAtSelection(state) {
4309
- const { $from } = state.selection;
4482
+ const { selection } = state;
4483
+ if (isNodeSelection(selection) && isNodeOfType(selection.node, "list")) return selection.node.attrs;
4484
+ const { $from } = selection;
4310
4485
  for (let depth = $from.depth; depth > 0; depth--) {
4311
4486
  const node = $from.node(depth);
4312
- if (isNodeOfType(node, "list")) return node.attrs;
4487
+ if (isNodeOfType(node, "list")) return $from.index(depth) === 0 ? node.attrs : null;
4313
4488
  }
4314
4489
  return null;
4315
4490
  }
@@ -4340,18 +4515,18 @@ function cycleCheckableList() {
4340
4515
  function cycleBulletOrderedList() {
4341
4516
  return (state, dispatch, view) => {
4342
4517
  const attrs = getListAttrsAtSelection(state);
4343
- const next = attrs?.kind === "bullet" || attrs?.kind === "ordered" ? {
4518
+ if (attrs?.kind === "bullet" || attrs?.kind === "ordered") return toggleList({
4344
4519
  kind: "ordered",
4345
4520
  marker: null,
4346
4521
  checked: false,
4347
4522
  collapsed: false
4348
- } : {
4523
+ })(state, dispatch, view);
4524
+ return wrapInList({
4349
4525
  kind: "bullet",
4350
4526
  marker: null,
4351
4527
  checked: false,
4352
4528
  collapsed: false
4353
- };
4354
- return toggleList(next)(state, dispatch, view);
4529
+ })(state, dispatch, view);
4355
4530
  };
4356
4531
  }
4357
4532
  function toggleListCollapsed() {
@@ -4575,7 +4750,9 @@ var MathMarkView = class {
4575
4750
  });
4576
4751
  }
4577
4752
  };
4578
- /** Inline math rendering: a KaTeX preview on the `mdMath` mark. */
4753
+ /**
4754
+ * Inline math rendering: a KaTeX preview on the `mdMath` mark.
4755
+ */
4579
4756
  function defineMath() {
4580
4757
  return defineMarkView({
4581
4758
  name: "mdMath",
@@ -4884,7 +5061,9 @@ function defineMoveBlock() {
4884
5061
  //#endregion
4885
5062
  //#region src/extensions/pending-replacement.ts
4886
5063
  const pendingReplacementKey = new PluginKey("meowdownPendingReplacement");
4887
- /** The active pending replacement, or null when there is none. */
5064
+ /**
5065
+ * The active pending replacement, or null when there is none.
5066
+ */
4888
5067
  function getPendingReplacement(state) {
4889
5068
  return pendingReplacementKey.getState(state)?.pending ?? null;
4890
5069
  }
@@ -5026,7 +5205,9 @@ function definePendingReplacementCommands() {
5026
5205
  discardPendingReplacement
5027
5206
  });
5028
5207
  }
5029
- /** Accept on Mod-Enter and discard on Escape, only while a replacement is pending. */
5208
+ /**
5209
+ * Accept on Mod-Enter and discard on Escape, only while a replacement is pending.
5210
+ */
5030
5211
  function definePendingReplacementKeymap() {
5031
5212
  return defineKeymap({
5032
5213
  "Mod-Enter": acceptPendingReplacement(),
@@ -5343,22 +5524,30 @@ function defineEditorExtension(options = {}) {
5343
5524
 
5344
5525
  //#endregion
5345
5526
  //#region src/extensions/schema.ts
5346
- /** The schema shared by every parser and serializer, built once and cached. */
5527
+ /**
5528
+ * The schema shared by every parser and serializer, built once and cached.
5529
+ */
5347
5530
  const getSharedSchema = /* @__PURE__ */ once(() => {
5348
5531
  const schema = defineEditorExtension().schema;
5349
5532
  if (schema == null) throw new Error("Unexpected empty schema");
5350
5533
  return schema;
5351
5534
  });
5352
- /** Typed node builders bound to the shared schema. */
5535
+ /**
5536
+ * Typed node builders bound to the shared schema.
5537
+ */
5353
5538
  const getNodeBuilders = /* @__PURE__ */ once(() => {
5354
5539
  return createNodeBuilders(getSharedSchema());
5355
5540
  });
5356
- /** Typed mark builders bound to the shared schema. */
5541
+ /**
5542
+ * Typed mark builders bound to the shared schema.
5543
+ */
5357
5544
  const getMarkBuilders = /* @__PURE__ */ once(() => {
5358
5545
  return createMarkBuilders(getSharedSchema());
5359
5546
  });
5360
5547
  const MARK_BUILDERS_CACHE_KEY = "meowdown_mark_builders";
5361
- /** Typed mark builders bound to a specific schema, cached per schema. */
5548
+ /**
5549
+ * Typed mark builders bound to a specific schema, cached per schema.
5550
+ */
5362
5551
  function getMarkBuildersForSchema(schema) {
5363
5552
  const cached = schema.cached[MARK_BUILDERS_CACHE_KEY];
5364
5553
  if (cached) return cached;
@@ -5367,7 +5556,9 @@ function getMarkBuildersForSchema(schema) {
5367
5556
  return builders;
5368
5557
  }
5369
5558
  const NODE_BUILDERS_CACHE_KEY = "meowdown_node_builders";
5370
- /** Typed node builders bound to a specific schema, cached per schema. */
5559
+ /**
5560
+ * Typed node builders bound to a specific schema, cached per schema.
5561
+ */
5371
5562
  function getNodeBuildersForSchema(schema) {
5372
5563
  const cached = schema.cached[NODE_BUILDERS_CACHE_KEY];
5373
5564
  if (cached) return cached;
@@ -5512,7 +5703,9 @@ function convertHeading(nodes, cursor, text, level, isSetext) {
5512
5703
  closingHashes
5513
5704
  }, content);
5514
5705
  }
5515
- /** Count the `=` / `-` characters in a setext underline run. */
5706
+ /**
5707
+ * Count the `=` / `-` characters in a setext underline run.
5708
+ */
5516
5709
  function countUnderlineChars(text, from, to) {
5517
5710
  if (from < 0) return 0;
5518
5711
  let count = 0;
@@ -5522,7 +5715,9 @@ function countUnderlineChars(text, from, to) {
5522
5715
  }
5523
5716
  return count;
5524
5717
  }
5525
- /** Count the `#` characters between `from` and `to`. */
5718
+ /**
5719
+ * Count the `#` characters between `from` and `to`.
5720
+ */
5526
5721
  function countHashChars(text, from, to) {
5527
5722
  if (from < 0) return 0;
5528
5723
  let count = 0;
@@ -5540,7 +5735,9 @@ function measureContentColumn(text, from) {
5540
5735
  for (let index = lineStart; index < from; index++) col += text.charCodeAt(index) === 9 ? 4 - col % 4 : 1;
5541
5736
  return col;
5542
5737
  }
5543
- /** Drop a line's leading whitespace up to `column`, counting a tab as `4 - col % 4` columns. */
5738
+ /**
5739
+ * Drop a line's leading whitespace up to `column`, counting a tab as `4 - col % 4` columns.
5740
+ */
5544
5741
  function sliceColumn(line, column) {
5545
5742
  let col = 0;
5546
5743
  let index = 0;
@@ -5632,7 +5829,9 @@ function convertList(nodes, cursor, text, kind) {
5632
5829
  }
5633
5830
  return items;
5634
5831
  }
5635
- /** The marker style at `cursor`, plus the start number of an ordered item. */
5832
+ /**
5833
+ * The marker style at `cursor`, plus the start number of an ordered item.
5834
+ */
5636
5835
  function readListMark(cursor, text, kind) {
5637
5836
  if (kind === "ordered") {
5638
5837
  const delimiterCode = text.charCodeAt(cursor.to - 1);
@@ -5859,7 +6058,9 @@ function canonicalizeTableRow(line) {
5859
6058
  function normalizeLine(line) {
5860
6059
  return canonicalizeTableRow(line) ?? collapseWhitespace(line);
5861
6060
  }
5862
- /** Classify how `markdown` survives the editor's parse-then-serialize round trip. */
6061
+ /**
6062
+ * Classify how `markdown` survives the editor's parse-then-serialize round trip.
6063
+ */
5863
6064
  function checkRoundTrip(markdown, options = {}) {
5864
6065
  const doc = markdownToDoc(markdown, { frontmatter: options.frontmatter });
5865
6066
  const serialized = docToMarkdown(doc, { frontmatter: options.frontmatter });
@@ -6036,7 +6237,9 @@ function applyTweetHeight(iframe, height) {
6036
6237
  const YOUTUBE_HOSTS = /^(?:www\.|m\.)?(?:youtube\.com|youtube-nocookie\.com)$/i;
6037
6238
  const YOUTU_BE_HOST = /^(?:www\.)?youtu\.be$/i;
6038
6239
  const VIDEO_ID = /^[\w-]{11}$/;
6039
- /** Extract `{ videoId, startSeconds? }` from any watch/shorts/embed/live/`youtu.be` URL. */
6240
+ /**
6241
+ * Extract `{ videoId, startSeconds? }` from any watch/shorts/embed/live/`youtu.be` URL.
6242
+ */
6040
6243
  function parseYouTube(src) {
6041
6244
  let url;
6042
6245
  try {
@@ -6059,7 +6262,9 @@ function parseYouTube(src) {
6059
6262
  startSeconds
6060
6263
  };
6061
6264
  }
6062
- /** `90`, `90s`, `1m30s`, `1h2m3s` to seconds. */
6265
+ /**
6266
+ * `90`, `90s`, `1m30s`, `1h2m3s` to seconds.
6267
+ */
6063
6268
  function parseStartSeconds(value) {
6064
6269
  if (/^\d+$/.test(value)) return Number(value);
6065
6270
  const matched = /^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/.exec(value);
@@ -6085,7 +6290,9 @@ const matchYouTube = (src) => {
6085
6290
  //#endregion
6086
6291
  //#region src/extensions/embed.ts
6087
6292
  const EMBED_MATCHERS = [matchYouTube, matchTweet];
6088
- /** Detect a tweet/YouTube embed in an image `src`, or `undefined` for a plain image. */
6293
+ /**
6294
+ * Detect a tweet/YouTube embed in an image `src`, or `undefined` for a plain image.
6295
+ */
6089
6296
  function matchEmbed(src) {
6090
6297
  for (const match of EMBED_MATCHERS) {
6091
6298
  const descriptor = match(src);
@@ -6170,7 +6377,9 @@ function createExitBoundaryPlugin(onExitBoundary) {
6170
6377
  } }
6171
6378
  });
6172
6379
  }
6173
- /** Call `onExitBoundary` when an arrow key press would leave the document boundary. */
6380
+ /**
6381
+ * Call `onExitBoundary` when an arrow key press would leave the document boundary.
6382
+ */
6174
6383
  function defineExitBoundaryHandler(onExitBoundary) {
6175
6384
  return withPriority$1(definePlugin(createExitBoundaryPlugin(onExitBoundary)), Priority$1.low);
6176
6385
  }
@@ -6253,7 +6462,9 @@ function takePastedFiles(data, options) {
6253
6462
  const defaultOnFileSaveError = (error) => {
6254
6463
  console.error("[meowdown] failed to save pasted file:", error);
6255
6464
  };
6256
- /** Escape `\`, `[`, and `]` so a filename stays inside its `[text]` label. */
6465
+ /**
6466
+ * Escape `\`, `[`, and `]` so a filename stays inside its `[text]` label.
6467
+ */
6257
6468
  function escapeLinkText(name) {
6258
6469
  return name.replaceAll(/[\\[\]]/g, String.raw`\$&`);
6259
6470
  }
@@ -6337,7 +6548,9 @@ function formatFileSize(bytes) {
6337
6548
 
6338
6549
  //#endregion
6339
6550
  //#region src/extensions/file-view.ts
6340
- /** `data-file-kind` values by file extension, for host CSS theming. */
6551
+ /**
6552
+ * `data-file-kind` values by file extension, for host CSS theming.
6553
+ */
6341
6554
  const FILE_KIND_BY_EXTENSION = /* @__PURE__ */ new Map([
6342
6555
  ["pdf", "pdf"],
6343
6556
  ["zip", "archive"],
@@ -6368,7 +6581,9 @@ const FILE_KIND_BY_EXTENSION = /* @__PURE__ */ new Map([
6368
6581
  ["txt", "text"],
6369
6582
  ["md", "text"]
6370
6583
  ]);
6371
- /** Classify a file destination for the pill's `data-file-kind` attribute. */
6584
+ /**
6585
+ * Classify a file destination for the pill's `data-file-kind` attribute.
6586
+ */
6372
6587
  function getFileKind(href) {
6373
6588
  const path = href.split(/[?#]/, 1)[0];
6374
6589
  const dot = path.lastIndexOf(".");
@@ -6377,7 +6592,9 @@ function getFileKind(href) {
6377
6592
  return FILE_KIND_BY_EXTENSION.get(extension) ?? "generic";
6378
6593
  }
6379
6594
  const SVG_NS = "http://www.w3.org/2000/svg";
6380
- /** A minimal document-outline icon, drawn in `currentColor`. */
6595
+ /**
6596
+ * A minimal document-outline icon, drawn in `currentColor`.
6597
+ */
6381
6598
  function buildFileIcon() {
6382
6599
  const svg = document.createElementNS(SVG_NS, "svg");
6383
6600
  svg.setAttribute("class", "md-file-view-icon");
@@ -6546,7 +6763,9 @@ function defineTagClickHandler(onClick) {
6546
6763
  //#endregion
6547
6764
  //#region src/extensions/wikilink-click.ts
6548
6765
  const wikilinkClickKey = new PluginKey("meowdown-wikilink-click");
6549
- /** Exported for tests. */
6766
+ /**
6767
+ * Exported for tests.
6768
+ */
6550
6769
  function findWikilinkAt(state, pos) {
6551
6770
  const range = getMarkRangeAt(state, pos, "mdWikilink");
6552
6771
  if (!range) return;
@@ -6676,7 +6895,9 @@ function findImageForPreview(view, preview) {
6676
6895
  if (!content) return;
6677
6896
  return findImageAt(view.state, view.posAtDOM(content, 0));
6678
6897
  }
6679
- /** Fingers wander a little during a tap; past this it is a scroll or a drag. */
6898
+ /**
6899
+ * Fingers wander a little during a tap; past this it is a scroll or a drag.
6900
+ */
6680
6901
  const TAP_MOVE_TOLERANCE = 10;
6681
6902
  function findTouch(touches, identifier) {
6682
6903
  return Array.from(touches).find((touch) => touch.identifier === identifier);
@@ -6767,7 +6988,9 @@ function defineImageClickHandler(onClick) {
6767
6988
 
6768
6989
  //#endregion
6769
6990
  //#region src/extensions/image.ts
6770
- /** Show an `src` as-is when it is an http(s) URL, otherwise skip rendering it. */
6991
+ /**
6992
+ * Show an `src` as-is when it is an http(s) URL, otherwise skip rendering it.
6993
+ */
6771
6994
  function defaultResolveImageUrl(src) {
6772
6995
  return /^https?:\/\//i.test(src) ? src : void 0;
6773
6996
  }
@@ -6843,7 +7066,9 @@ function rewriteMagicComment(view, range, patch, addToHistory) {
6843
7066
  if (!addToHistory) transaction.setMeta("addToHistory", false);
6844
7067
  view.dispatch(transaction);
6845
7068
  }
6846
- /** Persist a resized width and height into the trailing magic comment. */
7069
+ /**
7070
+ * Persist a resized width and height into the trailing magic comment.
7071
+ */
6847
7072
  function commitImageSize(view, content, rawWidth, rawHeight) {
6848
7073
  const pos = view.posAtDOM(content, 0);
6849
7074
  const range = getMarkRangeAt(view.state, pos, "mdImage");
@@ -6928,7 +7153,9 @@ var ImageMarkView = class {
6928
7153
  ignoreMutation(mutation) {
6929
7154
  return !this.#contentDOM.contains(mutation.target);
6930
7155
  }
6931
- /** Build the inline preview for the image `src`: an embed iframe or a resizable `<img>`. */
7156
+ /**
7157
+ * Build the inline preview for the image `src`: an embed iframe or a resizable `<img>`.
7158
+ */
6932
7159
  #renderPreview() {
6933
7160
  const { src } = this.#attrs;
6934
7161
  const embed = matchEmbed(src);
@@ -7038,7 +7265,9 @@ function defineImage(options = {}) {
7038
7265
 
7039
7266
  //#endregion
7040
7267
  //#region src/extensions/key-bindings.ts
7041
- /** Human-readable descriptions of the editor's formatting and heading shortcuts. */
7268
+ /**
7269
+ * Human-readable descriptions of the editor's formatting and heading shortcuts.
7270
+ */
7042
7271
  const EDITOR_KEY_BINDINGS = {
7043
7272
  "Mod-b": "Bold",
7044
7273
  "Mod-i": "Italic",
@@ -7390,7 +7619,9 @@ function defineSubstitutionEnterRules() {
7390
7619
  });
7391
7620
  }));
7392
7621
  }
7393
- /** Apply the editor's automatic plain-text substitutions. */
7622
+ /**
7623
+ * Apply the editor's automatic plain-text substitutions.
7624
+ */
7394
7625
  function defineSubstitution() {
7395
7626
  return union(defineSubstitutionInputRules(), defineSubstitutionUndo(), defineSubstitutionEnterRules());
7396
7627
  }
@@ -7454,7 +7685,9 @@ if (typeof window !== "undefined") {
7454
7685
  function getIsTouchInput() {
7455
7686
  return lastIsTouchInput;
7456
7687
  }
7457
- /** Calls `listener` whenever {@link getIsTouchInput} may report a new value. */
7688
+ /**
7689
+ * Calls `listener` whenever {@link getIsTouchInput} may report a new value.
7690
+ */
7458
7691
  function onIsTouchInputChange(listener) {
7459
7692
  listeners.add(listener);
7460
7693
  return () => {