@meowdown/core 0.64.1 → 0.64.3

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 +65 -26
  2. package/dist/index.js +224 -178
  3. package/package.json +2 -2
package/dist/index.d.ts CHANGED
@@ -246,31 +246,59 @@ interface MdMathAttrs {
246
246
  formula: string;
247
247
  }
248
248
  /**
249
- * mdPack keys for units that store no extra data; their own marks carry it.
250
- */
251
- type MdPackSimpleKey = 'bold' | 'italic' | 'code' | 'strike' | 'highlight' | 'autolink' | 'math' | 'wikilink' | 'image' | 'file';
252
- /**
253
- * Content-derived identity of one inline syntax unit. Adjacent units of the
254
- * same kind are kept apart by it (so they do not merge into one mark run), and
255
- * it stays stable when unrelated text in the block is edited, so editing one
256
- * unit never re-marks the others. `data` carries the unit's parsed payload (a
257
- * link's `href`/`title`) so callers read it off the mark instead of re-parsing
258
- * the text. The parser sets `slot: 1` on a unit whose pack would otherwise
259
- * equal the pack of the unit ending exactly where it starts; equal packs would
260
- * merge the two units into one mark run and one mark view.
261
- */
262
- type MdPackAttrs = ({
249
+ * Content-derived identity of one inline syntax unit.
250
+ *
251
+ * - `key`: the unit's kind. It keeps adjacent same-kind units apart and stays
252
+ * stable when unrelated text in the block is edited, so editing one unit
253
+ * never re-marks the others.
254
+ * - `data`: the unit's parsed data, read off the mark instead of
255
+ * re-parsing the text.
256
+ * - `slot`: `1` when the pack would otherwise equal the pack of the unit
257
+ * ending exactly where this one starts; equal packs would merge the two
258
+ * units into one mark run and one mark view.
259
+ * - `revealInFocus`/`revealInHide`: the mark modes in which the unit's
260
+ * source reveals around the caret.
261
+ */
262
+ type MdPackAttrs = {
263
+ key: 'italic' | 'bold' | 'code' | 'del' | 'highlight';
264
+ data?: null;
265
+ slot?: 1 | null;
266
+ revealInFocus: true;
267
+ revealInHide?: null;
268
+ } | {
263
269
  key: 'link';
264
270
  data: {
271
+ form: 'inline' | 'reference';
265
272
  href: string;
266
273
  title: string;
267
- reference?: true;
274
+ } | {
275
+ form: 'angle';
276
+ href: string;
277
+ };
278
+ slot?: 1 | null;
279
+ revealInFocus: true;
280
+ revealInHide?: null;
281
+ } | {
282
+ key: 'link';
283
+ data: {
284
+ form: 'bare';
285
+ href: string;
268
286
  };
287
+ slot?: 1 | null;
288
+ revealInFocus?: null;
289
+ revealInHide?: null;
290
+ } | {
291
+ key: 'math';
292
+ data?: null;
293
+ slot?: 1 | null;
294
+ revealInFocus: true;
295
+ revealInHide: true;
269
296
  } | {
270
- key: MdPackSimpleKey;
297
+ key: 'wikilink' | 'image' | 'file';
271
298
  data?: null;
272
- }) & {
273
299
  slot?: 1 | null;
300
+ revealInFocus?: null;
301
+ revealInHide?: null;
274
302
  };
275
303
  //#endregion
276
304
  //#region src/utils/range.d.ts
@@ -280,7 +308,7 @@ interface PositionRange {
280
308
  }
281
309
  //#endregion
282
310
  //#region src/extensions/get-link-unit-at.d.ts
283
- interface LinkUnit {
311
+ interface LinkUnitBase {
284
312
  /**
285
313
  * Whole inline link, reference link, or autolink range.
286
314
  */
@@ -292,14 +320,6 @@ interface LinkUnit {
292
320
  * whose collapsed glyphs measure at bogus coordinates.
293
321
  */
294
322
  text: PositionRange;
295
- /**
296
- * Interior of `[ ]`. Absent for an autolink.
297
- */
298
- label?: PositionRange;
299
- /**
300
- * Interior of `( )`. What `updateLink` rewrites. Absent for an autolink.
301
- */
302
- dest?: PositionRange;
303
323
  /**
304
324
  * The link URL. Could be an empty string.
305
325
  */
@@ -309,6 +329,25 @@ interface LinkUnit {
309
329
  */
310
330
  title: string;
311
331
  }
332
+ type LinkUnit = (LinkUnitBase & {
333
+ form: 'inline';
334
+ /**
335
+ * Interior of `[ ]`.
336
+ */
337
+ label: PositionRange;
338
+ /**
339
+ * Interior of `( )`. What `updateLink` rewrites.
340
+ */
341
+ dest: PositionRange;
342
+ }) | (LinkUnitBase & {
343
+ /**
344
+ * A reference link or autolink resolves an href but has no editable
345
+ * label/dest.
346
+ */
347
+ form: 'reference' | 'angle' | 'bare';
348
+ label?: undefined;
349
+ dest?: undefined;
350
+ });
312
351
  /**
313
352
  * The link covering `pos`, with its sub-ranges (`label`, `dest`) and parsed
314
353
  * `href`/`title`. The single query the commands and the hover/click handlers
package/dist/index.js CHANGED
@@ -88,22 +88,25 @@ function hasPointerSelectionTransaction(transactions) {
88
88
  function isMarkOfType(mark, name) {
89
89
  return mark.type.name === name;
90
90
  }
91
- const SYNTAX_MARK_NAMES = /* @__PURE__ */ new Set([
91
+ function isMarkOfTypes(mark, names) {
92
+ return names.includes(mark.type.name);
93
+ }
94
+ const SYNTAX_MARK_NAMES = [
92
95
  "mdMark",
93
96
  "mdLinkUri",
94
97
  "mdLinkTitle"
95
- ]);
96
- const ATOM_MARK_NAMES = /* @__PURE__ */ new Set([
98
+ ];
99
+ const ATOM_MARK_NAMES = [
97
100
  "mdWikilink",
98
101
  "mdImage",
99
102
  "mdFile",
100
103
  "mdMath"
101
- ]);
102
- const ATOM_PACK_KEYS = /* @__PURE__ */ new Set([
103
- "wikilink",
104
- "image",
105
- "file"
106
- ]);
104
+ ];
105
+ const ATOM_SOURCE_MARK_NAMES = [
106
+ "mdImage",
107
+ "mdWikilink",
108
+ "mdFile"
109
+ ];
107
110
 
108
111
  //#endregion
109
112
  //#region src/extensions/mark-mode.ts
@@ -123,9 +126,8 @@ function createMarkModePlugin(initialMode) {
123
126
  return { "data-mark-mode": getCurrentMarkMode(state) ?? initialMode };
124
127
  },
125
128
  decorations: (state) => {
126
- const mode = getCurrentMarkMode(state);
127
- if (mode === "focus") return computeFocusDecorations(state);
128
- if (mode === "hide") return computeMathRevealDecorations(state);
129
+ const mode = getCurrentMarkMode(state) ?? initialMode;
130
+ return mode === "focus" || mode === "hide" ? computeRevealDecorations(state, mode) : void 0;
129
131
  }
130
132
  }
131
133
  });
@@ -137,9 +139,6 @@ function setMarkMode(mode) {
137
139
  return true;
138
140
  };
139
141
  }
140
- function defineMarkMode(mode) {
141
- return union(definePlugin(createMarkModePlugin(mode)), defineCommands({ setMarkMode }));
142
- }
143
142
  /**
144
143
  * The active mark mode. `defineEditorExtension` always applies
145
144
  * `defineMarkMode`, so this is `undefined` only for a state built without it.
@@ -147,51 +146,49 @@ function defineMarkMode(mode) {
147
146
  function getMarkMode(state) {
148
147
  return markModeKey.getState(state);
149
148
  }
150
- /**
151
- * In focus mode, reveal the markdown syntax of the inline unit under the caret.
152
- *
153
- * Every revealable unit (emphasis, strong, code, strikethrough, link, autolink,
154
- * image) carries one `mdPack` mark spanning it, so a single boundary-inclusive
155
- * `getMarkRange` finds the unit, returning the outermost when units nest. One
156
- * decoration over its range flips the hidden punctuation/url/source visible via
157
- * the `.show` CSS rule. Because the range covers the whole unit, a caret at
158
- * either edge (e.g. right after a link's `)`) still reveals it. Atom packs
159
- * (`ATOM_PACK_KEYS`) and `#tag` never reveal.
160
- */
161
- function computeFocusDecorations(state) {
162
- return computeRevealDecorations(state, void 0);
163
- }
164
- /**
165
- * In hide mode, reveal only the math unit under the caret. A math unit hides
166
- * its whole source (content included), so it is the one construct that must
167
- * still reveal in hide mode to stay editable; everything else follows the
168
- * hide-mode contract and never reveals.
169
- */
170
- function computeMathRevealDecorations(state) {
171
- return computeRevealDecorations(state, { key: "math" });
172
- }
173
- function computeRevealDecorations(state, packAttrs) {
174
- const { selection } = state;
175
- if (!selection.empty) return DecorationSet.empty;
176
- const $pos = selection.$head;
149
+ function findRevealablePack($pos, mode, direction) {
177
150
  const { parent } = $pos;
178
- if (!parent.isTextblock || parent.type.spec.code) return DecorationSet.empty;
179
- const range = getRevealablePackRange(state, $pos, packAttrs);
180
- if (!range) return DecorationSet.empty;
181
- return DecorationSet.create(state.doc, [Decoration.inline(range.from, range.to, { class: "show" })]);
151
+ if (!parent.isTextblock || parent.type.spec.code) return;
152
+ const node = direction === -1 ? $pos.nodeBefore : $pos.nodeAfter;
153
+ for (const mark of node?.marks ?? []) if (isMarkOfType(mark, "mdPack")) {
154
+ const attrs = mark.attrs;
155
+ if (mode === "focus" && attrs.revealInFocus || mode === "hide" && attrs.revealInHide) return mark;
156
+ }
182
157
  }
183
- function isAtomPack(mark) {
184
- return ATOM_PACK_KEYS.has(mark.attrs.key);
158
+ /**
159
+ * Reveal the markdown syntax of the inline units the selection touches: the
160
+ * unit before its start and the unit after its end, which for a caret are
161
+ * the two units meeting at it.
162
+ *
163
+ * Every unit carries one `mdPack` mark spanning it, so one `getMarkRange`
164
+ * per probed pack expands it to its whole unit, and one decoration over
165
+ * each range flips the hidden punctuation/url/source visible via the `.show`
166
+ * CSS rule. The two probes land on the same unit when the selection sits
167
+ * inside one (equal ranges dedupe to one decoration), and on two units at a
168
+ * shared boundary, so the characters an edit would touch are never hidden.
169
+ * `#tag` carries no pack and never reveals.
170
+ */
171
+ function computeRevealDecorations(state, mode) {
172
+ const { $from, $to } = state.selection;
173
+ const packBefore = findRevealablePack($from, mode, -1);
174
+ const packAfter = findRevealablePack($to, mode, 1);
175
+ const ranges = [];
176
+ for (const [$pos, pack] of [[$from, packBefore], [$to, packAfter]]) {
177
+ if (!pack) continue;
178
+ const range = getMarkRange($pos, "mdPack", pack.attrs);
179
+ if (!range) continue;
180
+ if (ranges.some((r) => r.from === range.from && r.to === range.to)) continue;
181
+ ranges.push({
182
+ from: range.from,
183
+ to: range.to
184
+ });
185
+ }
186
+ if (ranges.length === 0) return;
187
+ const decorations = ranges.map((range) => Decoration.inline(range.from, range.to, { class: "show" }));
188
+ return DecorationSet.create(state.doc, decorations);
185
189
  }
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;
190
+ function defineMarkMode(mode) {
191
+ return union(definePlugin(createMarkModePlugin(mode)), defineCommands({ setMarkMode }));
195
192
  }
196
193
 
197
194
  //#endregion
@@ -206,7 +203,7 @@ function getCharMarks(state, pos) {
206
203
  function isHiddenChar(state, pos) {
207
204
  const marks = getCharMarks(state, pos);
208
205
  if (marks == null) return false;
209
- return marks.some((mark) => SYNTAX_MARK_NAMES.has(mark.type.name));
206
+ return marks.some((mark) => isMarkOfTypes(mark, SYNTAX_MARK_NAMES));
210
207
  }
211
208
  function isInsideNonCodeTextblock(state, pos) {
212
209
  if (pos < 0 || pos > state.doc.content.size) return false;
@@ -350,38 +347,35 @@ function resolvePosition(state, pos) {
350
347
  return $pos;
351
348
  }
352
349
  /**
353
- * Returns the run of the first listed mark covering `pos`, or `undefined` when
354
- * none covers it. `attrs` narrows the match to marks whose attributes contain
355
- * it, which is how a caller picks one level out of a nested stack of the same
356
- * mark.
350
+ * Returns the run of the mark covering `pos`, or `undefined` when none covers
351
+ * it. `attrs` narrows the match to marks whose attributes contain it, which
352
+ * is how a caller picks one level out of a nested stack of the same mark.
357
353
  */
358
354
  function getMarkRangeAt(state, pos, markName, attrs) {
359
355
  const $pos = resolvePosition(state, pos);
360
356
  if (!$pos) return;
361
- const markNames = Array.isArray(markName) ? markName : [markName];
362
- for (const name of markNames) {
363
- const range = getMarkRange($pos, name, attrs);
364
- if (range) return ATOM_MARK_NAMES.has(name) ? clampToUnitRange(state, $pos, range) : range;
365
- }
357
+ return ATOM_MARK_NAMES.includes(markName) ? getAtomUnitRange(state, $pos, markName) : getMarkRange($pos, markName, attrs);
366
358
  }
367
359
  /**
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.
360
+ * The range of the atom unit touching `$pos`, preferring the child to the
361
+ * right like `getMarkRange`. Two identical adjacent units carry equal atom
362
+ * marks, so eq-based expansion would run across both; the unit boundary comes
363
+ * from the innermost pack instead, which `slot` keeps distinct. A text node
364
+ * without a pack (a document not produced by the inline parser) falls back to
365
+ * the eq-based range. Does not support `attrs` filtering.
373
366
  */
374
- function clampToUnitRange(state, $pos, range) {
367
+ function getAtomUnitRange(state, $pos, name) {
375
368
  const parent = $pos.parent;
376
369
  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;
370
+ const matched = after.node && after.node.marks.some((mark) => isMarkOfType(mark, name)) ? after : parent.childBefore($pos.parentOffset);
371
+ const mark = matched.node?.marks.find((candidate) => isMarkOfType(candidate, name));
372
+ if (!matched.node || mark == null) return;
379
373
  const pack = getInnermostPackRangeAt(state, $pos.start() + matched.offset);
380
- if (!pack) return range;
374
+ if (!pack) return getMarkRange($pos, name);
381
375
  return {
382
- from: Math.max(range.from, pack.from),
383
- to: Math.min(range.to, pack.to),
384
- mark: range.mark
376
+ from: pack.from,
377
+ to: pack.to,
378
+ mark
385
379
  };
386
380
  }
387
381
  /**
@@ -424,15 +418,6 @@ function getMarkRangeStrictlyAround(state, pos, markNames) {
424
418
 
425
419
  //#endregion
426
420
  //#region src/extensions/atom-mark-navigation.ts
427
- /**
428
- * The source marks whose mark views hide the raw text behind a rendered
429
- * preview (`.md-atom-view-preview`) and act as one caret stop.
430
- */
431
- const ATOM_SOURCE_MARK_NAMES = [
432
- "mdImage",
433
- "mdWikilink",
434
- "mdFile"
435
- ];
436
421
  function getActiveMarkNames(marks, state) {
437
422
  const mode = getMarkMode(state);
438
423
  if (!mode) return [];
@@ -452,7 +437,7 @@ function getSelectedRange(state, markNames) {
452
437
  */
453
438
  function getSelectedAtomRange(state) {
454
439
  if (!getMarkMode(state)) return;
455
- return getSelectedRange(state, [...ATOM_SOURCE_MARK_NAMES]);
440
+ return getSelectedRange(state, ATOM_SOURCE_MARK_NAMES);
456
441
  }
457
442
  function findSelectionAcrossBlockBoundary(state, pos, markNames, direction) {
458
443
  const $pos = state.doc.resolve(pos);
@@ -789,13 +774,13 @@ function parsePositiveInteger(raw) {
789
774
  //#endregion
790
775
  //#region src/extensions/inline-runs.ts
791
776
  function findAtomMark(marks) {
792
- return marks.find((mark) => ATOM_MARK_NAMES.has(mark.type.name));
777
+ return marks.find((mark) => isMarkOfTypes(mark, ATOM_MARK_NAMES));
793
778
  }
794
779
  function findOwnPackMark(marks) {
795
780
  return marks.findLast((mark) => isMarkOfType(mark, "mdPack"));
796
781
  }
797
782
  function hasSyntaxMark(marks) {
798
- return marks.some((mark) => SYNTAX_MARK_NAMES.has(mark.type.name));
783
+ return marks.some((mark) => isMarkOfTypes(mark, SYNTAX_MARK_NAMES));
799
784
  }
800
785
  /**
801
786
  * Group a textblock's text nodes into atom units and plain runs. A unit's
@@ -2851,6 +2836,16 @@ const MARK_NAME_BY_TYPE_ID = /* @__PURE__ */ new Map([
2851
2836
  [LEZER_NODE_IDS.Hashtag, "mdTag"],
2852
2837
  [LEZER_NODE_IDS.WikilinkMark, "mdMark"]
2853
2838
  ]);
2839
+ function getGenericPackKey(type) {
2840
+ switch (type) {
2841
+ case LEZER_NODE_IDS.Emphasis: return "italic";
2842
+ case LEZER_NODE_IDS.StrongEmphasis: return "bold";
2843
+ case LEZER_NODE_IDS.InlineCode: return "code";
2844
+ case LEZER_NODE_IDS.Strikethrough: return "del";
2845
+ case LEZER_NODE_IDS.Highlight: return "highlight";
2846
+ default: return;
2847
+ }
2848
+ }
2854
2849
  /**
2855
2850
  * Walk a textblock's inline content and produce a list of mark chunks
2856
2851
  * with positions relative to the start of `text` (i.e. zero-based).
@@ -2913,6 +2908,7 @@ function walkNode(node, parentMarks, text, marks, out, options, context) {
2913
2908
  case LEZER_NODE_IDS.Wikilink: return walkWikilink(node, parentMarks, text, marks, out);
2914
2909
  case LEZER_NODE_IDS.WikiEmbed: return walkWikiEmbed(node, parentMarks, text, marks, out, options);
2915
2910
  case LEZER_NODE_IDS.InlineMath: return walkMath(node, parentMarks, text, marks, out);
2911
+ case LEZER_NODE_IDS.Autolink: return walkAutolink(node, parentMarks, text, marks, out);
2916
2912
  case LEZER_NODE_IDS.URL: return walkURL(node, parentMarks, text, marks, out);
2917
2913
  default: return walkGenericNode(node, parentMarks, text, marks, out, options, context);
2918
2914
  }
@@ -2922,29 +2918,67 @@ function walkNode(node, parentMarks, text, marks, out, options, context) {
2922
2918
  * syntax marks, then recurses into its children.
2923
2919
  */
2924
2920
  function walkGenericNode(node, parentMarks, text, marks, out, options, context) {
2925
- const type = node.type;
2926
- let packKey;
2927
- if (type === LEZER_NODE_IDS.Emphasis) packKey = "italic";
2928
- else if (type === LEZER_NODE_IDS.StrongEmphasis) packKey = "bold";
2929
- else if (type === LEZER_NODE_IDS.InlineCode) packKey = "code";
2930
- else if (type === LEZER_NODE_IDS.Strikethrough) packKey = "strike";
2931
- else if (type === LEZER_NODE_IDS.Highlight) packKey = "highlight";
2932
- else if (type === LEZER_NODE_IDS.Autolink) packKey = "autolink";
2933
- const base = packKey ? [...parentMarks, createUnitPack(marks, out, parentMarks, node.from, { key: packKey })] : parentMarks;
2934
- const maybeMarkName = MARK_NAME_BY_TYPE_ID.get(type);
2921
+ const packKey = getGenericPackKey(node.type);
2922
+ const packMark = packKey && createUnitPack(marks, out, parentMarks, node.from, {
2923
+ key: packKey,
2924
+ revealInFocus: true
2925
+ });
2926
+ const base = packMark ? [...parentMarks, packMark] : parentMarks;
2927
+ const maybeMarkName = MARK_NAME_BY_TYPE_ID.get(node.type);
2935
2928
  const childMarks = maybeMarkName ? [...base, marks[maybeMarkName].create()] : base;
2936
2929
  if (node.children.length === 0) emit(out, node.from, node.to, childMarks);
2937
2930
  else walk(node.children, childMarks, node.from, node.to, text, marks, out, options, context);
2938
2931
  }
2939
2932
  /**
2940
- * A standalone `URL` node is a GFM autolink (the address part of a real
2941
- * `[text](url)` is handled inside `walkResolvedLink`, not here). Linkify the
2942
- * shapes we recognize; anything else keeps the muted `mdLinkUri`.
2933
+ * Special walker for an angle autolink `<url>`. The unit's `href` lives on
2934
+ * its pack, computed from the `URL` child (`''` when the address is not
2935
+ * linkable, in which case the URL keeps the muted `mdLinkUri`). The children
2936
+ * are only the `<`/`>` marks and the `URL`, consumed right here: the `URL`
2937
+ * is a component of this unit, never a unit of its own.
2938
+ */
2939
+ function walkAutolink(node, parentMarks, text, marks, out) {
2940
+ const urlNode = node.children.find((child) => child.type === LEZER_NODE_IDS.URL);
2941
+ const href = urlNode ? getAutolinkHref(text.slice(urlNode.from, urlNode.to)) ?? "" : "";
2942
+ const base = [...parentMarks, createUnitPack(marks, out, parentMarks, node.from, {
2943
+ key: "link",
2944
+ data: {
2945
+ form: "angle",
2946
+ href
2947
+ },
2948
+ revealInFocus: true
2949
+ })];
2950
+ let pos = node.from;
2951
+ for (const child of node.children) {
2952
+ if (child.from > pos) emit(out, pos, child.from, base);
2953
+ const childMark = child.type === LEZER_NODE_IDS.URL ? href ? marks.mdLinkText.create({ href }) : marks.mdLinkUri.create() : marks.mdMark.create();
2954
+ emit(out, child.from, child.to, [...base, childMark]);
2955
+ pos = child.to;
2956
+ }
2957
+ if (pos < node.to) emit(out, pos, node.to, base);
2958
+ }
2959
+ /**
2960
+ * A `URL` node reaching this walker is a standalone GFM autolink: the address
2961
+ * part of a `[text](url)` link is consumed by `walkResolvedLink`, and an
2962
+ * angle autolink's URL by `walkAutolink`. Linkify the shapes we recognize,
2963
+ * packing each as its own unit; anything else keeps the muted `mdLinkUri`.
2943
2964
  */
2944
2965
  function walkURL(node, parentMarks, text, marks, out) {
2945
2966
  const href = getAutolinkHref(text.slice(node.from, node.to));
2946
- const mark = href ? marks.mdLinkText.create({ href }) : marks.mdLinkUri.create();
2947
- emit(out, node.from, node.to, [...parentMarks, mark]);
2967
+ if (!href) {
2968
+ emit(out, node.from, node.to, [...parentMarks, marks.mdLinkUri.create()]);
2969
+ return;
2970
+ }
2971
+ emit(out, node.from, node.to, [
2972
+ ...parentMarks,
2973
+ createUnitPack(marks, out, parentMarks, node.from, {
2974
+ key: "link",
2975
+ data: {
2976
+ form: "bare",
2977
+ href
2978
+ }
2979
+ }),
2980
+ marks.mdLinkText.create({ href })
2981
+ ]);
2948
2982
  }
2949
2983
  function walkLink(node, parentMarks, text, marks, out, options, context) {
2950
2984
  const parts = scanLinkParts(node);
@@ -2968,6 +3002,9 @@ function walkLink(node, parentMarks, text, marks, out, options, context) {
2968
3002
  * Locate the pieces of a `Link` node in Lezer's flat child list:
2969
3003
  * LinkMark `[`, [label children], LinkMark `]`, LinkMark `(`, URL,
2970
3004
  * optional LinkTitle, LinkMark `)`.
3005
+ *
3006
+ * An autolink inside the label also emits a `URL` child, so only a `URL`
3007
+ * after the second `LinkMark` (the `]` closing the label) is the destination.
2971
3008
  */
2972
3009
  function scanLinkParts(node) {
2973
3010
  let labelFrom = -1;
@@ -2984,7 +3021,7 @@ function scanLinkParts(node) {
2984
3021
  bracketCount++;
2985
3022
  if (bracketCount === 1) labelFrom = child.to;
2986
3023
  if (bracketCount === 2) labelTo = child.from;
2987
- } else if (urlNode == null && childType === LEZER_NODE_IDS.URL) urlNode = child;
3024
+ } else if (urlNode == null && bracketCount >= 2 && childType === LEZER_NODE_IDS.URL) urlNode = child;
2988
3025
  else if (titleNode == null && childType === LEZER_NODE_IDS.LinkTitle) titleNode = child;
2989
3026
  else if (referenceLabelNode == null && childType === LEZER_NODE_IDS.LinkLabel) referenceLabelNode = child;
2990
3027
  }
@@ -3083,17 +3120,14 @@ function walkResolvedLink(node, parts, resolution, parentMarks, text, marks, out
3083
3120
  const { href, title, isReference } = resolution;
3084
3121
  const linkTextMark = marks.mdLinkText.create({ href });
3085
3122
  const inLabel = (pos) => labelEnd >= 0 && pos < labelEnd;
3086
- const data = isReference ? {
3087
- href,
3088
- title,
3089
- reference: true
3090
- } : {
3091
- href,
3092
- title
3093
- };
3094
3123
  const pack = createUnitPack(marks, out, parentMarks, node.from, {
3095
3124
  key: "link",
3096
- data
3125
+ data: {
3126
+ form: isReference ? "reference" : "inline",
3127
+ href,
3128
+ title
3129
+ },
3130
+ revealInFocus: true
3097
3131
  });
3098
3132
  const base = [...parentMarks, pack];
3099
3133
  let pos = node.from;
@@ -3123,6 +3157,11 @@ function walkResolvedLink(node, parts, resolution, parentMarks, text, marks, out
3123
3157
  pos = child.to;
3124
3158
  continue;
3125
3159
  }
3160
+ if (child.type === LEZER_NODE_IDS.URL && inLabel(child.from)) {
3161
+ emit(out, child.from, child.to, baseForChild);
3162
+ pos = child.to;
3163
+ continue;
3164
+ }
3126
3165
  const maybeMarkName = MARK_NAME_BY_TYPE_ID.get(child.type);
3127
3166
  const childMarks = maybeMarkName ? [...baseForChild, marks[maybeMarkName].create()] : baseForChild;
3128
3167
  if (child.children.length === 0) emit(out, child.from, child.to, childMarks);
@@ -3192,7 +3231,11 @@ function walkMath(node, parentMarks, text, marks, out) {
3192
3231
  const formula = text.slice(markNodes[0].to, markNodes[1].from);
3193
3232
  const base = [
3194
3233
  ...parentMarks,
3195
- createUnitPack(marks, out, parentMarks, node.from, { key: "math" }),
3234
+ createUnitPack(marks, out, parentMarks, node.from, {
3235
+ key: "math",
3236
+ revealInFocus: true,
3237
+ revealInHide: true
3238
+ }),
3196
3239
  marks.mdMath.create({ formula })
3197
3240
  ];
3198
3241
  emit(out, node.from, markNodes[0].to, [...base, marks.mdMark.create()]);
@@ -3710,10 +3753,11 @@ function defineMdMath() {
3710
3753
  }
3711
3754
  /**
3712
3755
  * 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.
3716
- * `excludes: ''` lets nested units carry two of these marks at once.
3756
+ * strikethrough, inline and angle link, math) focus mode reveals the unit
3757
+ * with one range lookup instead of stitching its punctuation back together;
3758
+ * a unit that never reveals (wikilink, image, file, bare autolink) carries
3759
+ * it as unit identity. `excludes: ''` lets nested units carry two of these
3760
+ * marks at once.
3717
3761
  */
3718
3762
  function defineMdPack() {
3719
3763
  return defineMarkSpec({
@@ -3723,7 +3767,9 @@ function defineMdPack() {
3723
3767
  attrs: {
3724
3768
  key: {},
3725
3769
  data: { default: null },
3726
- slot: { default: null }
3770
+ slot: { default: null },
3771
+ revealInFocus: { default: null },
3772
+ revealInHide: { default: null }
3727
3773
  },
3728
3774
  toDOM: (mark) => {
3729
3775
  return [
@@ -4105,66 +4151,66 @@ function lastMarkRunIn(state, range, markName) {
4105
4151
  * the `mdLinkUri` run locates the `( )` body.
4106
4152
  */
4107
4153
  function getLinkUnitAt(state, pos) {
4108
- const linkText = getMarkRangeAt(state, pos, "mdLinkText");
4109
- const pack = getMarkRangeAt(state, pos, "mdPack", { key: "link" }) ?? getMarkRangeAt(state, pos, "mdPack", { key: "autolink" });
4110
- const unit = pack ?? linkText;
4154
+ const unit = getMarkRangeAt(state, pos, "mdPack", { key: "link" });
4111
4155
  if (!unit) return;
4112
- const packAttrs = pack?.mark.attrs;
4113
- const href = (linkText?.mark.attrs)?.href ?? "";
4114
- if (!pack || packAttrs?.key !== "link") {
4115
- const unitRange = {
4116
- from: unit.from,
4117
- to: unit.to
4156
+ const { data } = unit.mark.attrs;
4157
+ const unitRange = {
4158
+ from: unit.from,
4159
+ to: unit.to
4160
+ };
4161
+ switch (data.form) {
4162
+ case "bare": return {
4163
+ form: "bare",
4164
+ unit: unitRange,
4165
+ text: unitRange,
4166
+ href: data.href,
4167
+ title: ""
4118
4168
  };
4119
- return {
4169
+ case "angle": return {
4170
+ form: "angle",
4120
4171
  unit: unitRange,
4121
- text: packAttrs?.key === "autolink" ? lastMarkRunIn(state, unitRange, "mdLinkText") ?? {
4172
+ text: {
4122
4173
  from: unit.from + 1,
4123
4174
  to: unit.to - 1
4124
- } : unitRange,
4125
- href,
4126
- title: ""
4127
- };
4128
- }
4129
- if (packAttrs.data.reference === true) {
4130
- const text = linkText == null ? {
4131
- from: unit.from,
4132
- to: unit.to
4133
- } : {
4134
- from: linkText.from + 1,
4135
- to: linkText.to
4136
- };
4137
- return {
4138
- unit: {
4139
- from: unit.from,
4140
- to: unit.to
4141
4175
  },
4142
- text,
4143
- href: packAttrs.data.href,
4144
- title: packAttrs.data.title
4176
+ href: data.href,
4177
+ title: ""
4145
4178
  };
4179
+ case "reference": {
4180
+ const linkText = getMarkRangeAt(state, pos, "mdLinkText");
4181
+ return {
4182
+ form: "reference",
4183
+ unit: unitRange,
4184
+ text: linkText == null ? unitRange : {
4185
+ from: linkText.from + 1,
4186
+ to: linkText.to
4187
+ },
4188
+ href: data.href,
4189
+ title: data.title
4190
+ };
4191
+ }
4192
+ case "inline": {
4193
+ const uri = lastMarkRunIn(state, unitRange, "mdLinkUri");
4194
+ const closeBracket = uri ? uri.from - 2 : unit.to - 3;
4195
+ const destFrom = uri ? uri.from : unit.to - 1;
4196
+ const label = {
4197
+ from: unit.from + 1,
4198
+ to: closeBracket
4199
+ };
4200
+ return {
4201
+ form: "inline",
4202
+ unit: unitRange,
4203
+ text: label,
4204
+ label,
4205
+ dest: {
4206
+ from: destFrom,
4207
+ to: unit.to - 1
4208
+ },
4209
+ href: data.href,
4210
+ title: data.title
4211
+ };
4212
+ }
4146
4213
  }
4147
- const uri = lastMarkRunIn(state, unit, "mdLinkUri");
4148
- const closeBracket = uri ? uri.from - 2 : unit.to - 3;
4149
- const destFrom = uri ? uri.from : unit.to - 1;
4150
- const label = {
4151
- from: unit.from + 1,
4152
- to: closeBracket
4153
- };
4154
- return {
4155
- unit: {
4156
- from: unit.from,
4157
- to: unit.to
4158
- },
4159
- text: label,
4160
- label,
4161
- dest: {
4162
- from: destFrom,
4163
- to: unit.to - 1
4164
- },
4165
- href: packAttrs.data.href,
4166
- title: packAttrs.data.title
4167
- };
4168
4214
  }
4169
4215
 
4170
4216
  //#endregion
@@ -4224,7 +4270,7 @@ function insertLink({ href, title, wrapText = true } = {}) {
4224
4270
  function updateLink(attrs) {
4225
4271
  return (state, dispatch) => {
4226
4272
  const link = getLinkUnitAt(state, state.selection.from);
4227
- if (!link?.dest) return false;
4273
+ if (link?.form !== "inline") return false;
4228
4274
  const dest = destText(normalizeHref(attrs.href ?? link.href), attrs.title ?? link.title);
4229
4275
  if (dispatch) dispatch(state.tr.insertText(dest, link.dest.from, link.dest.to).scrollIntoView());
4230
4276
  return true;
@@ -4236,7 +4282,7 @@ function updateLink(attrs) {
4236
4282
  function removeLink() {
4237
4283
  return (state, dispatch) => {
4238
4284
  const link = getLinkUnitAt(state, state.selection.from);
4239
- if (!link?.label) return false;
4285
+ if (link?.form !== "inline") return false;
4240
4286
  if (dispatch) dispatch(state.tr.delete(link.label.to, link.unit.to).delete(link.unit.from, link.label.from).scrollIntoView());
4241
4287
  return true;
4242
4288
  };
@@ -4252,7 +4298,7 @@ function openLinkEdit(onLinkEdit) {
4252
4298
  return (state, dispatch, view) => {
4253
4299
  const link = getLinkUnitAt(state, state.selection.from);
4254
4300
  if (link) {
4255
- if (link.label == null || link.dest == null) return false;
4301
+ if (link.form !== "inline") return false;
4256
4302
  if (dispatch && view) {
4257
4303
  const { unit: { from, to } } = link;
4258
4304
  dispatch(state.tr.setSelection(TextSelection.create(state.doc, from, to)).scrollIntoView());
@@ -7900,7 +7946,7 @@ function cleanTextFromSlice(slice, options = {}) {
7900
7946
  blockNode.descendants((textNode) => {
7901
7947
  if (!textNode.isText || !textNode.text) return true;
7902
7948
  const textNodeMarks = textNode.marks.map((mark) => mark.type.name);
7903
- if (!(textNodeMarks.some((markName) => SYNTAX_MARK_NAMES.has(markName)) && !(options.preserveMathSource && textNodeMarks.includes("mdMath")))) parts.push(textNode.text);
7949
+ if (!(textNodeMarks.some((markName) => SYNTAX_MARK_NAMES.includes(markName)) && !(options.preserveMathSource && textNodeMarks.includes("mdMath")))) parts.push(textNode.text);
7904
7950
  return false;
7905
7951
  });
7906
7952
  blocks.push(parts.join(""));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meowdown/core",
3
3
  "type": "module",
4
- "version": "0.64.1",
4
+ "version": "0.64.3",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -43,7 +43,7 @@
43
43
  "remark-stringify": "^11.0.0",
44
44
  "unicode-by-name": "^0.2.0",
45
45
  "unified": "^11.0.5",
46
- "@meowdown/markdown": "^0.64.1"
46
+ "@meowdown/markdown": "^0.64.3"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@ocavue/tsconfig": "^0.7.1",