mossmd 0.8.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 (119) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/LICENSE +21 -0
  3. package/README.md +336 -0
  4. package/dist/collab/index.d.ts +14 -0
  5. package/dist/collab/index.d.ts.map +1 -0
  6. package/dist/collab/index.js +6 -0
  7. package/dist/collab/index.js.map +1 -0
  8. package/dist/core/code-languages.d.ts +3 -0
  9. package/dist/core/code-languages.d.ts.map +1 -0
  10. package/dist/core/code-languages.js +130 -0
  11. package/dist/core/code-languages.js.map +1 -0
  12. package/dist/core/decoration-utils.d.ts +4 -0
  13. package/dist/core/decoration-utils.d.ts.map +1 -0
  14. package/dist/core/decoration-utils.js +25 -0
  15. package/dist/core/decoration-utils.js.map +1 -0
  16. package/dist/core/edit-helpers.d.ts +13 -0
  17. package/dist/core/edit-helpers.d.ts.map +1 -0
  18. package/dist/core/edit-helpers.js +318 -0
  19. package/dist/core/edit-helpers.js.map +1 -0
  20. package/dist/core/icons.d.ts +16 -0
  21. package/dist/core/icons.d.ts.map +1 -0
  22. package/dist/core/icons.js +86 -0
  23. package/dist/core/icons.js.map +1 -0
  24. package/dist/core/inline-preview.d.ts +32 -0
  25. package/dist/core/inline-preview.d.ts.map +1 -0
  26. package/dist/core/inline-preview.js +925 -0
  27. package/dist/core/inline-preview.js.map +1 -0
  28. package/dist/core/list-editing.d.ts +10 -0
  29. package/dist/core/list-editing.d.ts.map +1 -0
  30. package/dist/core/list-editing.js +244 -0
  31. package/dist/core/list-editing.js.map +1 -0
  32. package/dist/core/list-model.d.ts +40 -0
  33. package/dist/core/list-model.d.ts.map +1 -0
  34. package/dist/core/list-model.js +114 -0
  35. package/dist/core/list-model.js.map +1 -0
  36. package/dist/core/list-navigation.d.ts +3 -0
  37. package/dist/core/list-navigation.d.ts.map +1 -0
  38. package/dist/core/list-navigation.js +234 -0
  39. package/dist/core/list-navigation.js.map +1 -0
  40. package/dist/core/markdown-context.d.ts +3 -0
  41. package/dist/core/markdown-context.d.ts.map +1 -0
  42. package/dist/core/markdown-context.js +10 -0
  43. package/dist/core/markdown-context.js.map +1 -0
  44. package/dist/core/preview-activity.d.ts +18 -0
  45. package/dist/core/preview-activity.d.ts.map +1 -0
  46. package/dist/core/preview-activity.js +199 -0
  47. package/dist/core/preview-activity.js.map +1 -0
  48. package/dist/core/preview-widgets.d.ts +65 -0
  49. package/dist/core/preview-widgets.d.ts.map +1 -0
  50. package/dist/core/preview-widgets.js +359 -0
  51. package/dist/core/preview-widgets.js.map +1 -0
  52. package/dist/core/read-only.d.ts +26 -0
  53. package/dist/core/read-only.d.ts.map +1 -0
  54. package/dist/core/read-only.js +49 -0
  55. package/dist/core/read-only.js.map +1 -0
  56. package/dist/core/tree-progress.d.ts +29 -0
  57. package/dist/core/tree-progress.d.ts.map +1 -0
  58. package/dist/core/tree-progress.js +143 -0
  59. package/dist/core/tree-progress.js.map +1 -0
  60. package/dist/editor.d.ts +219 -0
  61. package/dist/editor.d.ts.map +1 -0
  62. package/dist/editor.js +551 -0
  63. package/dist/editor.js.map +1 -0
  64. package/dist/features/callout/index.d.ts +9 -0
  65. package/dist/features/callout/index.d.ts.map +1 -0
  66. package/dist/features/callout/index.js +223 -0
  67. package/dist/features/callout/index.js.map +1 -0
  68. package/dist/features/file-blocks/index.d.ts +12 -0
  69. package/dist/features/file-blocks/index.d.ts.map +1 -0
  70. package/dist/features/file-blocks/index.js +397 -0
  71. package/dist/features/file-blocks/index.js.map +1 -0
  72. package/dist/features/image/index.d.ts +28 -0
  73. package/dist/features/image/index.d.ts.map +1 -0
  74. package/dist/features/image/index.js +699 -0
  75. package/dist/features/image/index.js.map +1 -0
  76. package/dist/features/index.d.ts +17 -0
  77. package/dist/features/index.d.ts.map +1 -0
  78. package/dist/features/index.js +9 -0
  79. package/dist/features/index.js.map +1 -0
  80. package/dist/features/search/index.d.ts +4 -0
  81. package/dist/features/search/index.d.ts.map +1 -0
  82. package/dist/features/search/index.js +131 -0
  83. package/dist/features/search/index.js.map +1 -0
  84. package/dist/features/slash-commands/index.d.ts +52 -0
  85. package/dist/features/slash-commands/index.d.ts.map +1 -0
  86. package/dist/features/slash-commands/index.js +422 -0
  87. package/dist/features/slash-commands/index.js.map +1 -0
  88. package/dist/features/table/index.d.ts +44 -0
  89. package/dist/features/table/index.d.ts.map +1 -0
  90. package/dist/features/table/index.js +1262 -0
  91. package/dist/features/table/index.js.map +1 -0
  92. package/dist/features/upload/index.d.ts +80 -0
  93. package/dist/features/upload/index.d.ts.map +1 -0
  94. package/dist/features/upload/index.js +790 -0
  95. package/dist/features/upload/index.js.map +1 -0
  96. package/dist/features/wiki-links/index.d.ts +27 -0
  97. package/dist/features/wiki-links/index.d.ts.map +1 -0
  98. package/dist/features/wiki-links/index.js +577 -0
  99. package/dist/features/wiki-links/index.js.map +1 -0
  100. package/dist/index.d.ts +17 -0
  101. package/dist/index.d.ts.map +1 -0
  102. package/dist/index.js +12 -0
  103. package/dist/index.js.map +1 -0
  104. package/dist/styles/content.css +247 -0
  105. package/dist/styles/inline-preview.css +2116 -0
  106. package/dist/styles/tokens.css +234 -0
  107. package/dist/syntax/highlight.d.ts +10 -0
  108. package/dist/syntax/highlight.d.ts.map +1 -0
  109. package/dist/syntax/highlight.js +75 -0
  110. package/dist/syntax/highlight.js.map +1 -0
  111. package/dist/syntax/index.d.ts +16 -0
  112. package/dist/syntax/index.d.ts.map +1 -0
  113. package/dist/syntax/index.js +29 -0
  114. package/dist/syntax/index.js.map +1 -0
  115. package/dist/theme/index.d.ts +6 -0
  116. package/dist/theme/index.d.ts.map +1 -0
  117. package/dist/theme/index.js +204 -0
  118. package/dist/theme/index.js.map +1 -0
  119. package/package.json +237 -0
@@ -0,0 +1,925 @@
1
+ import { ensureSyntaxTree, syntaxTree } from '@codemirror/language';
2
+ import { Decoration, EditorView, ViewPlugin, WidgetType, } from '@codemirror/view';
3
+ import { treeGrowthEffect, treeProgressPlugin } from './tree-progress';
4
+ import { readOnlyFacet } from './read-only';
5
+ import { pushReplace } from './decoration-utils';
6
+ import { defaultOnLinkClick, getPreviewActivity, linkElementFromEvent, linkIconHitTarget, previewActivityExtension, previewFrozenField, refreshInlinePreview, shouldRevealTaskSource, } from './preview-activity';
7
+ import { BulletWidget, CodeCopyWidget, fencedCodeSource, parseListTaskMarker, taskCheckboxConfigFacet, TaskCheckboxWidget, shouldUseNativeTaskCheckbox, } from './preview-widgets';
8
+ import { listEditingExtension } from './list-editing';
9
+ import { listNavigationExtension } from './list-navigation';
10
+ import { isLineInsideMarkdownCode } from './markdown-context';
11
+ function parseExtendedHorizontalRule(text) {
12
+ const match = text.match(/^\s*(-{3,}|\*{3,}|_{3,})(.*?)\1\s*$/);
13
+ if (!match)
14
+ return null;
15
+ const glyph = match[2].trim();
16
+ if (!glyph)
17
+ return null;
18
+ return {
19
+ marker: match[1][0],
20
+ glyph,
21
+ };
22
+ }
23
+ function isPlainHorizontalRule(text) {
24
+ return /^\s*(\*|_|-)(?:[ \t]*\1){2,}[ \t]*$/.test(text);
25
+ }
26
+ class HorizontalRuleWidget extends WidgetType {
27
+ constructor(marker, glyph = null) {
28
+ super();
29
+ Object.defineProperty(this, "marker", {
30
+ enumerable: true,
31
+ configurable: true,
32
+ writable: true,
33
+ value: marker
34
+ });
35
+ Object.defineProperty(this, "glyph", {
36
+ enumerable: true,
37
+ configurable: true,
38
+ writable: true,
39
+ value: glyph
40
+ });
41
+ }
42
+ eq(other) {
43
+ return other.marker === this.marker && other.glyph === this.glyph;
44
+ }
45
+ toDOM() {
46
+ const wrap = document.createElement('span');
47
+ const classes = ['cm-moss-hr-widget'];
48
+ if (this.marker === '*')
49
+ classes.push('cm-moss-hr-widget-wavy');
50
+ if (this.glyph)
51
+ classes.push('cm-moss-hr-widget-glyph');
52
+ wrap.className = classes.join(' ');
53
+ wrap.setAttribute('aria-hidden', 'true');
54
+ const left = document.createElement('span');
55
+ left.className = 'cm-moss-hr-segment cm-moss-hr-segment-left';
56
+ if (!this.glyph) {
57
+ wrap.append(left);
58
+ return wrap;
59
+ }
60
+ const glyph = document.createElement('span');
61
+ glyph.className = 'cm-moss-hr-symbol';
62
+ glyph.textContent = this.glyph;
63
+ const right = document.createElement('span');
64
+ right.className = 'cm-moss-hr-segment cm-moss-hr-segment-right';
65
+ wrap.append(left, glyph, right);
66
+ return wrap;
67
+ }
68
+ }
69
+ function horizontalRuleClassNames(marker, glyph) {
70
+ const classNames = ['cm-moss-hr'];
71
+ if (marker === '*')
72
+ classNames.push('cm-moss-hr-wavy');
73
+ if (glyph)
74
+ classNames.push('cm-moss-hr-glyph');
75
+ return classNames.join(' ');
76
+ }
77
+ function pushHorizontalRuleWidget(ranges, doc, line, marker, glyph = null) {
78
+ pushReplace(ranges, doc, line.from, line.to, {
79
+ widget: new HorizontalRuleWidget(marker, glyph),
80
+ });
81
+ }
82
+ // decoration building
83
+ const LINE_CLASS_BY_BLOCK = {
84
+ ATXHeading1: 'cm-moss-h1',
85
+ ATXHeading2: 'cm-moss-h2',
86
+ ATXHeading3: 'cm-moss-h3',
87
+ ATXHeading4: 'cm-moss-h4',
88
+ ATXHeading5: 'cm-moss-h5',
89
+ ATXHeading6: 'cm-moss-h6',
90
+ SetextHeading1: 'cm-moss-h1',
91
+ SetextHeading2: 'cm-moss-h2',
92
+ Blockquote: 'cm-moss-blockquote',
93
+ FencedCode: 'cm-moss-fenced-code',
94
+ };
95
+ const HIDEABLE_SYNTAX = new Set([
96
+ 'HeaderMark',
97
+ 'EmphasisMark',
98
+ 'CodeMark',
99
+ 'CodeInfo',
100
+ 'LinkMark',
101
+ 'LinkTitle',
102
+ 'StrikethroughMark',
103
+ 'HighlightMark',
104
+ 'QuoteMark',
105
+ ]);
106
+ // Children of a Link node whose visibility follows the link-scoped
107
+ // rule (cursor-inside-link) instead of the default line-based rule.
108
+ // The same token names can appear under an Image node — those stay
109
+ // on the line-based rule because images are a different UX surface.
110
+ const LINK_CHILD_SYNTAX = new Set(['LinkMark', 'URL', 'LinkTitle']);
111
+ function isWikiLinkNode(node, doc) {
112
+ // Wiki links are parsed by the Markdown grammar as ordinary Link nodes
113
+ // whose range starts at the second `[` and ends at the first `]`. Their
114
+ // feature decoration owns both brackets and the cursor-scoped reveal.
115
+ // Letting this generic link pass also makes hidden wiki syntax participate
116
+ // in the DOM position mapping twice, which can map a click in following
117
+ // prose to the previous link's `]|]` boundary.
118
+ return (node.name === 'Link' &&
119
+ node.from > 0 &&
120
+ node.to < doc.length &&
121
+ doc.sliceString(node.from - 1, node.from + 1) === '[[' &&
122
+ doc.sliceString(node.to - 1, node.to + 1) === ']]');
123
+ }
124
+ function isCalloutMarkerNode(node, doc) {
125
+ if (node.name !== 'Link')
126
+ return false;
127
+ const line = doc.lineAt(node.from);
128
+ const match = line.text.match(/^(\s{0,3}>\s?)(\[![A-Za-z][\w-]*\])/);
129
+ return (match !== null &&
130
+ node.from === line.from + match[1].length &&
131
+ node.to === line.from + match[0].length);
132
+ }
133
+ const INLINE_MARK_CLASS = {
134
+ StrongEmphasis: 'cm-moss-strong',
135
+ Emphasis: 'cm-moss-em',
136
+ InlineCode: 'cm-moss-inline-code',
137
+ Strikethrough: 'cm-moss-strike',
138
+ Highlight: 'cm-moss-highlight',
139
+ Link: 'cm-moss-link',
140
+ };
141
+ // A Link can contain two URL nodes when its visible label is itself a
142
+ // URL: `[https://label](https://destination)`. Only the node after the
143
+ // closing `]` is the destination syntax that should collapse. Treating
144
+ // every URL under Link as a destination makes the visible label vanish.
145
+ function linkDestinationUrl(link, doc) {
146
+ const labelClose = link
147
+ .getChildren('LinkMark')
148
+ .find((mark) => doc.sliceString(mark.from, mark.to) === ']');
149
+ if (!labelClose)
150
+ return null;
151
+ return (link
152
+ .getChildren('URL')
153
+ .find((url) => url.from >= labelClose.to) ?? null);
154
+ }
155
+ const LIST_BASE_EM = 0.8;
156
+ const LIST_ALCOVE_EM = 1.2;
157
+ // Keep the visual step aligned with the existing list indentation contract.
158
+ const LIST_LEVEL_EM = 1;
159
+ function nearestListItem(node) {
160
+ for (let current = node; current; current = current.parent) {
161
+ if (current.name === 'ListItem')
162
+ return current;
163
+ }
164
+ return null;
165
+ }
166
+ function listItemDepth(item) {
167
+ let depth = 0;
168
+ for (let parent = item.parent; parent; parent = parent.parent) {
169
+ if (parent.name === 'ListItem')
170
+ depth++;
171
+ }
172
+ return depth;
173
+ }
174
+ function sameListItem(a, b) {
175
+ return a?.name === 'ListItem' && a.from === b.from && a.to === b.to;
176
+ }
177
+ function buildInlineDecorations(view) {
178
+ const { state } = view;
179
+ const { doc } = state;
180
+ const ranges = [];
181
+ // In read-only mode no line is ever "active" — the whole doc stays
182
+ // rendered (no reveal). We skip the selection walk entirely rather
183
+ // than relying on `hasFocus` staying false, so a programmatic
184
+ // `.focus()` can't accidentally reveal source under reading mode.
185
+ const { activeLines: initialActiveLines } = getPreviewActivity(view);
186
+ const activeLines = new Set(initialActiveLines);
187
+ // Decorate the whole parsed tree — not the current viewport — so
188
+ // that scrolling never needs to rebuild the decoration set. Prior
189
+ // design walked viewport-only and rebuilt on every scroll, which
190
+ // on iOS caused scroll-up momentum halts whenever new decorations
191
+ // were applied to lines at the top of the viewport (anchor
192
+ // conflict with the scroll animation). Cost: a one-shot whole-doc
193
+ // walk on every doc / selection / focus change instead of a
194
+ // smaller walk on every scroll.
195
+ //
196
+ // `ensureSyntaxTree(..., doc.length, ...)` guarantees the tree
197
+ // actually covers the whole doc before we walk it. Without this,
198
+ // for moderately long atoms the incremental parser's initial
199
+ // pass falls short of the end, we'd walk only a prefix, and
200
+ // content past that point renders as raw `##`/`**` forever —
201
+ // decorations don't rebuild on scroll anymore. Subsequent calls
202
+ // are near-free because ensureSyntaxTree short-circuits once the
203
+ // tree reaches the target.
204
+ const tree = ensureSyntaxTree(state, state.doc.length, 200) ?? syntaxTree(state);
205
+ const taskConfig = state.facet(taskCheckboxConfigFacet);
206
+ const extendedHorizontalRuleLines = new Map();
207
+ const horizontalRuleSourceLines = new Set();
208
+ for (let number = 1; number <= doc.lines; number++) {
209
+ const line = doc.line(number);
210
+ if (line.length === 0) {
211
+ ranges.push(Decoration.line({ class: 'cm-moss-empty-line' }).range(line.from));
212
+ }
213
+ if (isLineInsideMarkdownCode(state, number))
214
+ continue;
215
+ const extendedHorizontalRule = parseExtendedHorizontalRule(line.text);
216
+ if (!extendedHorizontalRule) {
217
+ if (!isPlainHorizontalRule(line.text))
218
+ continue;
219
+ horizontalRuleSourceLines.add(number);
220
+ if (activeLines.has(number) && line.from < line.to) {
221
+ ranges.push(Decoration.mark({ class: 'cm-moss-hr-source' }).range(line.from, line.to));
222
+ }
223
+ continue;
224
+ }
225
+ extendedHorizontalRuleLines.set(number, extendedHorizontalRule);
226
+ horizontalRuleSourceLines.add(number);
227
+ if (activeLines.has(number)) {
228
+ if (line.from < line.to) {
229
+ ranges.push(Decoration.mark({ class: 'cm-moss-hr-source' }).range(line.from, line.to));
230
+ }
231
+ continue;
232
+ }
233
+ ranges.push(Decoration.line({
234
+ class: horizontalRuleClassNames(extendedHorizontalRule.marker, extendedHorizontalRule.glyph),
235
+ }).range(line.from));
236
+ pushHorizontalRuleWidget(ranges, doc, line, extendedHorizontalRule.marker, extendedHorizontalRule.glyph);
237
+ }
238
+ // `from` positions of Link nodes whose range overlaps a selection.
239
+ // Link children (LinkMark/URL/LinkTitle) hide unless their parent
240
+ // Link's `from` is in this set — i.e. the cursor has entered or is
241
+ // immediately beside the link, not merely landed on the same line.
242
+ // Images aren't included because their widget owns the source visibility.
243
+ const activeLinkStarts = new Set();
244
+ // Single pre-order walk. A tree walk visits a parent before its
245
+ // children, which lets us compute two pieces of look-ahead state on
246
+ // the way in — right before the children that depend on them:
247
+ // - Fenced-code active expansion: clicking any line of a fence
248
+ // activates the whole block. FencedCode is entered before its
249
+ // CodeMark/CodeInfo children, so expanding activeLines here means
250
+ // those children hide/reveal consistently with the block.
251
+ // - activeLinkStarts: a Link is entered before its LinkMark/URL/
252
+ // LinkTitle children, so recording it here makes the link-scoped
253
+ // reveal rule ready when those children are processed.
254
+ // (A previous version ran a separate pre-pass plus a taskMarkerByLine
255
+ // map. Folding both into this one walk halves the per-rebuild tree
256
+ // traversal — meaningful because this runs on every cursor move and
257
+ // its cost scales with document size.)
258
+ tree.iterate({
259
+ enter: (node) => {
260
+ if (node.name !== 'Document' &&
261
+ (extendedHorizontalRuleLines.has(doc.lineAt(node.from).number) ||
262
+ (node.name !== 'HorizontalRule' &&
263
+ horizontalRuleSourceLines.has(doc.lineAt(node.from).number)))) {
264
+ return false;
265
+ }
266
+ // GFM parses custom task markers such as `[!]` and `[/]` as
267
+ // shortcut links. They are task UI, not links: skip the synthetic
268
+ // Link node so it cannot add a link icon or hide its source using
269
+ // link-scoped reveal rules.
270
+ if (node.name === 'Link') {
271
+ if (isWikiLinkNode(node, doc))
272
+ return false;
273
+ // Callout owns the `[!TYPE]` marker on blockquote lines. The
274
+ // Markdown parser exposes it as a shortcut Link, but treating it as
275
+ // a normal link would hide its brackets and apply link styling.
276
+ if (isCalloutMarkerNode(node, doc))
277
+ return false;
278
+ const line = doc.lineAt(node.from);
279
+ const taskInfo = parseListTaskMarker(line.text, taskConfig);
280
+ if (taskInfo &&
281
+ !isLineInsideMarkdownCode(state, line.number) &&
282
+ node.from === line.from + taskInfo.markerFrom &&
283
+ node.to === line.from + taskInfo.markerTo) {
284
+ return false;
285
+ }
286
+ }
287
+ if (node.name === 'FencedCode') {
288
+ const firstLine = doc.lineAt(node.from).number;
289
+ const lastLine = doc.lineAt(node.to).number;
290
+ let anyActive = false;
291
+ for (let n = firstLine; n <= lastLine; n++) {
292
+ if (activeLines.has(n)) {
293
+ anyActive = true;
294
+ break;
295
+ }
296
+ }
297
+ if (anyActive) {
298
+ for (let n = firstLine; n <= lastLine; n++)
299
+ activeLines.add(n);
300
+ }
301
+ ranges.push(Decoration.widget({
302
+ widget: new CodeCopyWidget(fencedCodeSource(doc, node.from, node.to)),
303
+ }).range(doc.line(firstLine).from));
304
+ }
305
+ if (node.name === 'Link' && view.hasFocus) {
306
+ for (const range of state.selection.ranges) {
307
+ // Inclusive overlap: cursor sitting exactly on either
308
+ // boundary counts as inside, matching the UX where the
309
+ // next keystroke affects the link.
310
+ if (range.from <= node.to && range.to >= node.from) {
311
+ activeLinkStarts.add(node.from);
312
+ break;
313
+ }
314
+ }
315
+ }
316
+ if (node.name === 'FencedCode') {
317
+ const firstLine = doc.lineAt(node.from);
318
+ const lastLine = doc.lineAt(node.to);
319
+ for (let n = firstLine.number; n <= lastLine.number; n++) {
320
+ const line = doc.line(n);
321
+ const classes = ['cm-moss-fenced-code'];
322
+ if (n === firstLine.number)
323
+ classes.push('cm-moss-fenced-code-start');
324
+ if (n === lastLine.number)
325
+ classes.push('cm-moss-fenced-code-end');
326
+ ranges.push(Decoration.line({ class: classes.join(' ') }).range(line.from));
327
+ }
328
+ }
329
+ else {
330
+ const lineClass = LINE_CLASS_BY_BLOCK[node.name];
331
+ if (lineClass) {
332
+ const firstLine = doc.lineAt(node.from);
333
+ const lastLine = doc.lineAt(node.to);
334
+ for (let n = firstLine.number; n <= lastLine.number; n++) {
335
+ const line = doc.line(n);
336
+ ranges.push(Decoration.line({ class: lineClass }).range(line.from));
337
+ }
338
+ }
339
+ }
340
+ const markClass = INLINE_MARK_CLASS[node.name];
341
+ if (markClass && node.from < node.to) {
342
+ ranges.push(Decoration.mark({ class: markClass }).range(node.from, node.to));
343
+ }
344
+ if (HIDEABLE_SYNTAX.has(node.name) && node.from < node.to) {
345
+ const lineNum = doc.lineAt(node.from).number;
346
+ // Link children use a link-scoped rule rather than the
347
+ // line-based rule. A LinkMark under an Image node falls
348
+ // through to line-based — images have their own widget UX.
349
+ let shouldHide;
350
+ if (LINK_CHILD_SYNTAX.has(node.name)) {
351
+ let parent = node.node.parent;
352
+ while (parent && parent.name !== 'Link' && parent.name !== 'Image') {
353
+ parent = parent.parent;
354
+ }
355
+ if (parent && parent.name === 'Link') {
356
+ shouldHide = !activeLinkStarts.has(parent.from);
357
+ }
358
+ else {
359
+ shouldHide = !activeLines.has(lineNum);
360
+ }
361
+ }
362
+ else {
363
+ shouldHide = !activeLines.has(lineNum);
364
+ }
365
+ if (shouldHide) {
366
+ let hideTo = node.to;
367
+ if (node.name === 'HeaderMark' || node.name === 'QuoteMark') {
368
+ while (hideTo < doc.length && doc.sliceString(hideTo, hideTo + 1) === ' ') {
369
+ hideTo++;
370
+ }
371
+ }
372
+ pushReplace(ranges, doc, node.from, hideTo);
373
+ }
374
+ }
375
+ if (node.name === 'URL' && node.from < node.to) {
376
+ const parent = node.node.parent;
377
+ if (parent?.name === 'Link') {
378
+ // A URL in the label is visible content. A URL after the
379
+ // closing `]` is destination syntax and follows the same
380
+ // cursor-inside-or-beside-this-link reveal rule.
381
+ const destination = linkDestinationUrl(parent, doc);
382
+ if (destination?.from === node.from &&
383
+ !activeLinkStarts.has(parent.from)) {
384
+ pushReplace(ranges, doc, node.from, node.to);
385
+ }
386
+ }
387
+ else {
388
+ // Bare GFM URLs and `<https://...>` autolinks are visible
389
+ // content, not syntax. Give them the same styling and icon
390
+ // hit target as explicit links while leaving their text in
391
+ // the document flow on inactive lines.
392
+ ranges.push(Decoration.mark({ class: 'cm-moss-link' }).range(node.from, node.to));
393
+ }
394
+ }
395
+ // Backslash escapes: `\.`, `\*`, `\(`, etc. RSS-to-markdown
396
+ // converters escape a lot of punctuation defensively, and the
397
+ // backslashes show through as literal chars without preview.
398
+ // Hide just the leading backslash on inactive lines so the
399
+ // escaped character remains visible — mirrors how Obsidian
400
+ // renders escapes. The Escape node spans both characters
401
+ // (`\` + escaped char), so we only replace the first position.
402
+ if (node.name === 'Escape' && node.to - node.from >= 2) {
403
+ const lineNum = doc.lineAt(node.from).number;
404
+ if (!activeLines.has(lineNum)) {
405
+ pushReplace(ranges, doc, node.from, node.from + 1);
406
+ }
407
+ }
408
+ if (node.name === 'ListMark' && node.from < node.to) {
409
+ const line = doc.lineAt(node.from);
410
+ const lineActive = activeLines.has(line.number);
411
+ const markText = doc.sliceString(node.from, node.to);
412
+ const orderedMarker = /^\d{1,9}[.)]$/.test(markText);
413
+ const markEndInLine = node.to - line.from;
414
+ const hasListSeparator = markEndInLine < line.text.length &&
415
+ /\s/.test(line.text[markEndInLine] ?? '');
416
+ // Lezer accepts an empty `1.` as an OrderedList so it can parse
417
+ // CommonMark's optional empty item. In the editor, keep that raw
418
+ // punctuation as prose until the user types the separator space.
419
+ if (orderedMarker && !hasListSeparator)
420
+ return;
421
+ // Detect task status from the raw line because the Markdown parser
422
+ // only knows the standard `[ ]` / `[x]` pair. This also lets the
423
+ // widget replace custom statuses before their source reaches the
424
+ // normal inline decoration pass.
425
+ const taskInfo = parseListTaskMarker(line.text, taskConfig);
426
+ const standardTask = taskInfo != null && (taskInfo.key === ' ' || taskInfo.key === 'x');
427
+ const revealTaskSource = taskInfo != null && shouldRevealTaskSource(view, line.from, taskInfo);
428
+ // Hanging-indent every physical line owned by this list item.
429
+ // Ownership and depth come from the parsed tree, not raw source
430
+ // indentation: CommonMark allows up to three leading spaces on a
431
+ // top-level item, and ordered-list children commonly use a
432
+ // marker-width indent rather than two spaces.
433
+ //
434
+ // Layout:
435
+ //
436
+ // <--BASE--><--ALCOVE--> first-line text
437
+ // • wrapped lines land at the
438
+ // same column as the first-line
439
+ // text, not back under the marker
440
+ //
441
+ // LIST_ALCOVE_EM is fixed regardless of list kind.
442
+ // Every marker (bullet widget, checkbox widget, ordered
443
+ // number via mark decoration) is forced into an
444
+ // inline-block of exactly that width via CSS — so the
445
+ // alignment math doesn't depend on per-font marker
446
+ // widths. `padding-left` sets the content column;
447
+ // negative `text-indent` of the same magnitude pulls the
448
+ // first line back so the marker lands in the alcove. Structural
449
+ // leading spaces are replaced visually on every owned line;
450
+ // otherwise they would be added on top of the tree-derived
451
+ // padding and ordered/odd indentation would still drift.
452
+ const listItem = nearestListItem(node.node);
453
+ const listDepth = listItem ? listItemDepth(listItem) : 0;
454
+ if (listItem) {
455
+ const padding = LIST_BASE_EM + LIST_ALCOVE_EM + listDepth * LIST_LEVEL_EM;
456
+ const firstLine = doc.lineAt(listItem.from);
457
+ const lastLine = doc.lineAt(listItem.to);
458
+ for (let number = firstLine.number; number <= lastLine.number; number++) {
459
+ const ownedLine = doc.line(number);
460
+ const contentOffset = ownedLine.text.search(/\S/);
461
+ if (contentOffset < 0)
462
+ continue;
463
+ const contentFrom = ownedLine.from + contentOffset;
464
+ const owner = nearestListItem(tree.resolve(contentFrom, 1));
465
+ if (!sameListItem(owner, listItem))
466
+ continue;
467
+ const markerLine = ownedLine.number === line.number;
468
+ if (!markerLine && contentFrom === ownedLine.from)
469
+ continue;
470
+ ranges.push(Decoration.line({
471
+ attributes: {
472
+ style: `padding-left: ${padding}em; text-indent: ${markerLine ? `-${LIST_ALCOVE_EM}` : '0'}em`,
473
+ },
474
+ }).range(ownedLine.from));
475
+ if (contentFrom > ownedLine.from) {
476
+ pushReplace(ranges, doc, ownedLine.from, contentFrom);
477
+ }
478
+ }
479
+ }
480
+ if (lineActive &&
481
+ (markText === '-' ||
482
+ markText === '*' ||
483
+ markText === '+' ||
484
+ orderedMarker) &&
485
+ // Task markers are rendered by the task-specific branches below.
486
+ // Only keep the raw list marker when the task source is actually
487
+ // being revealed; otherwise replacing just TaskMarker would leave
488
+ // the default `- ` visible while custom statuses replace the full
489
+ // list prefix and marker together.
490
+ (taskInfo == null || revealTaskSource)) {
491
+ ranges.push(Decoration.mark({
492
+ class: `cm-moss-list-marker ${lineActive ? 'cm-moss-list-marker-active ' : ''}${orderedMarker
493
+ ? 'cm-moss-ordered-marker'
494
+ : 'cm-moss-unordered-marker'}`,
495
+ }).range(node.from, node.to));
496
+ return;
497
+ }
498
+ // Figure out how far past node.to the mark's trailing
499
+ // space lives. For standard tasks, the `- ` span runs from
500
+ // node.from to the task marker, which is handled by the
501
+ // TaskMarker node below. Custom statuses are handled here
502
+ // because they are not represented by a parser TaskMarker.
503
+ // For bullets / ordered, include a single trailing space
504
+ // if present so text flows from padding-left without a
505
+ // spurious leading space.
506
+ const hasTrailingSpace = doc.sliceString(node.to, node.to + 1) === ' ';
507
+ const markEnd = hasTrailingSpace ? node.to + 1 : node.to;
508
+ if (taskInfo != null && !standardTask) {
509
+ pushReplace(ranges, doc, node.from, line.from + taskInfo.markerTo, {
510
+ widget: new TaskCheckboxWidget(taskInfo.key, taskInfo.raw, taskInfo.status, line.from + taskInfo.markerFrom, false),
511
+ });
512
+ if (taskInfo.separator.length > 0) {
513
+ pushReplace(ranges, doc, line.from + taskInfo.markerTo, line.from + taskInfo.markerTo + taskInfo.separator.length);
514
+ }
515
+ if (taskInfo.status.completed) {
516
+ ranges.push(Decoration.line({ class: 'cm-moss-task-done' }).range(line.from));
517
+ }
518
+ }
519
+ else if (taskInfo != null) {
520
+ // Hide `- ` (ListMark through the space before `[`).
521
+ pushReplace(ranges, doc, node.from, line.from + taskInfo.markerFrom);
522
+ }
523
+ else {
524
+ if (markText === '-' || markText === '*' || markText === '+') {
525
+ // Bullet: substitute with the fixed-width marker
526
+ // widget, swallowing the trailing space so content
527
+ // starts precisely at padding-left.
528
+ pushReplace(ranges, doc, node.from, markEnd, {
529
+ widget: new BulletWidget(listDepth),
530
+ });
531
+ }
532
+ else {
533
+ // Ordered list (or anything else with a non-standard
534
+ // mark text like `1.`, `42.`): keep both the marker and
535
+ // its source separator visible. The separator is part of
536
+ // the editable Markdown geometry and must not be replaced
537
+ // on inactive lines, otherwise activating the line changes
538
+ // the content column by a sub-pixel amount.
539
+ ranges.push(Decoration.mark({
540
+ class: `cm-moss-list-marker ${lineActive ? 'cm-moss-list-marker-active ' : ''}${orderedMarker
541
+ ? 'cm-moss-ordered-marker'
542
+ : 'cm-moss-unordered-marker'}`,
543
+ }).range(node.from, node.to));
544
+ }
545
+ }
546
+ }
547
+ // Tables are rendered by the separate `mossTables()` block-widget
548
+ // table feature (./features/table) — the whole Table range is
549
+ // replaced with an interactive HTML `<table>`. Any inline
550
+ // decorations on TableHeader/TableRow/TableDelimiter would
551
+ // target ranges that are already hidden behind the replace
552
+ // widget, so they're intentionally absent from this builder.
553
+ if (node.name === 'HorizontalRule') {
554
+ // CommonMark HR: a line of `***`, `---`, or `___` (3+, any
555
+ // spacing between). On inactive lines we hide the characters
556
+ // and render a horizontal-rule widget. On active
557
+ // lines we leave the raw characters visible so the user can
558
+ // edit the marker without it vanishing.
559
+ const line = doc.lineAt(node.from);
560
+ if (!activeLines.has(line.number)) {
561
+ const marker = line.text.trimStart().slice(0, 1);
562
+ const hrMarker = marker === '*' || marker === '_' ? marker : '-';
563
+ ranges.push(Decoration.line({
564
+ class: horizontalRuleClassNames(hrMarker, null),
565
+ }).range(line.from));
566
+ pushHorizontalRuleWidget(ranges, doc, line, hrMarker);
567
+ }
568
+ }
569
+ if (node.name === 'Image' && node.from < node.to) {
570
+ // Images stay widget-first even when the editor has focus. Their
571
+ // edit button and widget selection provide the editing affordance,
572
+ // while keeping the raw link hidden avoids a source row appearing
573
+ // above the rendered image.
574
+ //
575
+ // Keep the now-empty source `.cm-line` at its default line-height
576
+ // rather than collapsing it via `display: none`: on iOS Safari,
577
+ // toggling a line from its text-measured height to zero mid-scroll
578
+ // shifts every subsequent line and can halt kinetic momentum.
579
+ pushReplace(ranges, doc, node.from, node.to);
580
+ }
581
+ if (node.name === 'TaskMarker' && node.from < node.to) {
582
+ const lineNum = doc.lineAt(node.from).number;
583
+ const line = doc.line(lineNum);
584
+ const taskInfo = parseListTaskMarker(line.text, taskConfig);
585
+ if (!taskInfo)
586
+ return;
587
+ if (shouldRevealTaskSource(view, line.from, taskInfo))
588
+ return;
589
+ // Keep the marker and its separator as separate atomic ranges. If
590
+ // they are combined, ArrowLeft from the first body character skips
591
+ // to the start of the whole widget; after the source is revealed the
592
+ // caret then appears before `[x]` instead of after it. A separate
593
+ // hidden separator lets the caret stop at markerTo, which is the
594
+ // `- [x]| text` editing position, while preserving the fixed alcove
595
+ // width in preview mode.
596
+ const hasTrailingSpace = node.to < doc.length &&
597
+ doc.sliceString(node.to, node.to + 1) === ' ';
598
+ pushReplace(ranges, doc, node.from, node.to, {
599
+ widget: new TaskCheckboxWidget(taskInfo.key, taskInfo.raw, taskInfo.status, node.from, shouldUseNativeTaskCheckbox(taskInfo.key, taskConfig)),
600
+ });
601
+ if (hasTrailingSpace) {
602
+ pushReplace(ranges, doc, node.to, node.to + 1);
603
+ }
604
+ if (taskInfo.status.completed) {
605
+ ranges.push(Decoration.line({ class: 'cm-moss-task-done' }).range(line.from));
606
+ }
607
+ }
608
+ },
609
+ });
610
+ // Supplemental inline marks for the line containing the cursor.
611
+ // CommonMark's flanking rules say that `**foo **` is not emphasis
612
+ // because the closing `**` is preceded by whitespace — lezer
613
+ // agrees and doesn't emit `StrongEmphasis`, so the walk above
614
+ // misses it. Result: while the user types a sentence inside
615
+ // `**...**`, the bold styling flicks on and off every time they
616
+ // hit the spacebar. We patch the UX by scanning the active line
617
+ // for matched delimiter pairs the cursor sits between and
618
+ // emitting the mark ourselves regardless of flanking. Once the
619
+ // cursor leaves, lezer's opinion wins and the visual reverts to
620
+ // what will actually persist when the line is serialized.
621
+ if (view.hasFocus) {
622
+ const head = state.selection.main.head;
623
+ const line = doc.lineAt(head);
624
+ if (activeLines.has(line.number) &&
625
+ !horizontalRuleSourceLines.has(line.number)) {
626
+ supplementMidTypingEmphasis(line.text, line.from, head - line.from, ranges);
627
+ }
628
+ }
629
+ return Decoration.set(ranges, true);
630
+ }
631
+ // Delimiters we emit supplemental marks for, longest first so `**`
632
+ // is matched before `*` and `__` before `_`. Backticks don't need
633
+ // this treatment — CommonMark inline code isn't subject to
634
+ // flanking rules. Each entry carries both the content class (what
635
+ // lezer would style via `t.strong` / `t.emphasis` / `t.strikethrough`)
636
+ // and the delimiter class (matches how the EmphasisMark token
637
+ // renders when lezer *does* parse: parent tag's weight / style /
638
+ // decoration plus `processingInstruction`'s faint color).
639
+ const MID_TYPING_DELIMITERS = [
640
+ { delim: '**', contentCls: 'cm-moss-strong', delimCls: 'cm-moss-strong-mark' },
641
+ { delim: '__', contentCls: 'cm-moss-strong', delimCls: 'cm-moss-strong-mark' },
642
+ { delim: '~~', contentCls: 'cm-moss-strike', delimCls: 'cm-moss-strike-mark' },
643
+ { delim: '==', contentCls: 'cm-moss-highlight', delimCls: 'cm-moss-highlight-mark' },
644
+ { delim: '*', contentCls: 'cm-moss-em', delimCls: 'cm-moss-em-mark' },
645
+ { delim: '_', contentCls: 'cm-moss-em', delimCls: 'cm-moss-em-mark' },
646
+ ];
647
+ function supplementMidTypingEmphasis(text, lineFrom, localCursor, out) {
648
+ // Track which characters of the line are already "owned" by a
649
+ // matched delimiter pair so a single-char delimiter doesn't
650
+ // accidentally pair halves of two different double-delimiter
651
+ // spans.
652
+ const consumed = new Uint8Array(text.length);
653
+ for (const { delim, contentCls, delimCls } of MID_TYPING_DELIMITERS) {
654
+ const dLen = delim.length;
655
+ // Underscore emphasis (`_`, `__`) doesn't open intra-word under
656
+ // CommonMark's flanking rules — `snake_case_var` is not italic.
657
+ // Without this guard the supplement would flash false italic while
658
+ // the cursor sits between two intra-word underscores (exactly the
659
+ // flicker this feature exists to prevent, inverted). Asterisk
660
+ // delimiters have no such restriction, so only gate underscores.
661
+ const isUnderscore = delim === '_' || delim === '__';
662
+ let searchFrom = 0;
663
+ while (searchFrom < text.length) {
664
+ const open = indexOfUnconsumed(text, delim, searchFrom, consumed);
665
+ if (open < 0)
666
+ break;
667
+ if (isUnderscore && open > 0 && /\w/.test(text[open - 1])) {
668
+ searchFrom = open + dLen;
669
+ continue;
670
+ }
671
+ const close = indexOfUnconsumed(text, delim, open + dLen, consumed);
672
+ if (close < 0)
673
+ break;
674
+ for (let i = open; i < close + dLen; i++)
675
+ consumed[i] = 1;
676
+ const contentFrom = open + dLen;
677
+ const contentTo = close;
678
+ if (contentFrom < contentTo &&
679
+ localCursor > open &&
680
+ localCursor < close + dLen) {
681
+ out.push(Decoration.mark({ class: contentCls }).range(lineFrom + contentFrom, lineFrom + contentTo));
682
+ // Style the delimiter characters to match how lezer's
683
+ // `EmphasisMark` tokens render when the pattern parses
684
+ // cleanly. Lezer tags `EmphasisMark` with both its parent
685
+ // (`strong` / `emphasis` / `strikethrough`) and
686
+ // `processingInstruction`, so the `**` characters get
687
+ // faint color AND the parent's weight / style / decoration
688
+ // — we mirror all of that here so the delimiters don't
689
+ // flip style / size / color when the cursor moves or a
690
+ // trailing space triggers / untriggers lezer's parse.
691
+ out.push(Decoration.mark({ class: delimCls }).range(lineFrom + open, lineFrom + contentFrom));
692
+ out.push(Decoration.mark({ class: delimCls }).range(lineFrom + contentTo, lineFrom + close + dLen));
693
+ }
694
+ searchFrom = close + dLen;
695
+ }
696
+ }
697
+ }
698
+ function indexOfUnconsumed(text, needle, from, consumed) {
699
+ let i = from;
700
+ while (i <= text.length - needle.length) {
701
+ const found = text.indexOf(needle, i);
702
+ if (found < 0)
703
+ return -1;
704
+ let isConsumed = false;
705
+ for (let k = found; k < found + needle.length; k++) {
706
+ if (consumed[k]) {
707
+ isConsumed = true;
708
+ break;
709
+ }
710
+ }
711
+ if (!isConsumed)
712
+ return found;
713
+ i = found + 1;
714
+ }
715
+ return -1;
716
+ }
717
+ const inlinePreviewPlugin = ViewPlugin.fromClass(class {
718
+ constructor(view) {
719
+ Object.defineProperty(this, "decorations", {
720
+ enumerable: true,
721
+ configurable: true,
722
+ writable: true,
723
+ value: void 0
724
+ });
725
+ this.decorations = buildInlineDecorations(view);
726
+ }
727
+ update(update) {
728
+ const prevFrozen = update.startState.field(previewFrozenField);
729
+ const nextFrozen = update.state.field(previewFrozenField);
730
+ const justUnfroze = prevFrozen && !nextFrozen;
731
+ const composingTransaction = update.transactions.some((tr) => tr.isUserEvent('input.type.compose'));
732
+ if (update.docChanged &&
733
+ (composingTransaction ||
734
+ update.view.composing)) {
735
+ this.decorations = this.decorations.map(update.changes);
736
+ return;
737
+ }
738
+ // A doc change is unambiguous edit intent, so rebuild even while
739
+ // frozen. Returning the stale (pre-edit) decoration set here would
740
+ // hand CM6 ranges whose positions no longer match the document: a
741
+ // hidden `## ` replace can end up spanning the newly-typed text's
742
+ // line break ("Decorations that replace line breaks may not be
743
+ // specified via plugins"), and the stale positions corrupt the
744
+ // heightmap ("No tile at position …" → broken scrollIntoView). The
745
+ // freeze only needs to suppress the *selection*-driven reveal that
746
+ // makes a click jitter; typing should reveal syntax as normal.
747
+ if (nextFrozen && !justUnfroze && !update.docChanged)
748
+ return;
749
+ // Tree-growth effect: background parser advanced past where
750
+ // we last walked. For docs large enough that the initial
751
+ // parse didn't reach the end, later blocks (headings, lists,
752
+ // etc.) render as raw `##`/`**` until this fires.
753
+ let treeGrew = false;
754
+ let forceRefresh = false;
755
+ for (const tr of update.transactions) {
756
+ for (const effect of tr.effects) {
757
+ if (effect.is(refreshInlinePreview)) {
758
+ forceRefresh = true;
759
+ break;
760
+ }
761
+ if (effect.is(treeGrowthEffect)) {
762
+ treeGrew = true;
763
+ break;
764
+ }
765
+ }
766
+ if (treeGrew)
767
+ break;
768
+ }
769
+ // Note: `update.viewportChanged` is intentionally NOT in this
770
+ // list. Scrolling alone must not rebuild decorations — doing
771
+ // so on iOS halts momentum whenever the rebuild produces new
772
+ // decorations for lines at the top of a scroll-up viewport
773
+ // (CM6 anchor conflict with the scroll animation). Walking
774
+ // the whole parsed tree on the remaining triggers means
775
+ // scroll-time cost is zero; the tree walk itself is
776
+ // single-digit ms for typical atoms.
777
+ // A read-only toggle (compartment reconfigure) changes neither
778
+ // doc nor selection nor focus, so detect the facet flip directly
779
+ // — otherwise reading mode wouldn't repaint into / out of the
780
+ // fully-rendered state.
781
+ const readOnlyChanged = update.startState.facet(readOnlyFacet) !==
782
+ update.state.facet(readOnlyFacet);
783
+ if (forceRefresh ||
784
+ justUnfroze ||
785
+ update.docChanged ||
786
+ update.selectionSet ||
787
+ update.focusChanged ||
788
+ treeGrew ||
789
+ readOnlyChanged) {
790
+ this.decorations = buildInlineDecorations(update.view);
791
+ }
792
+ }
793
+ }, {
794
+ decorations: (v) => v.decorations,
795
+ eventHandlers: {
796
+ compositionstart(_event, view) {
797
+ view.dispatch({ effects: refreshInlinePreview.of() });
798
+ return false;
799
+ },
800
+ compositionend(_event, view) {
801
+ setTimeout(() => {
802
+ view.dispatch({ effects: refreshInlinePreview.of() });
803
+ }, 0);
804
+ },
805
+ },
806
+ });
807
+ // CM6's drawSelection layer intentionally sits behind `.cm-content`. That is
808
+ // normally ideal—the rectangle is behind the glyphs—but an opaque fenced-code
809
+ // line background also sits between the layer and the glyphs, hiding the
810
+ // selection completely. Mirror only the selected portions of FencedCode as
811
+ // inline marks so their background paints above the block and below its text.
812
+ // This plugin stays separate from inlinePreviewPlugin because mouse selection
813
+ // must repaint live even while preview decorations are frozen for click-jitter
814
+ // prevention.
815
+ function fencedCodeSelectionDecorations(view) {
816
+ const selections = view.state.selection.ranges.filter((range) => !range.empty);
817
+ if (selections.length === 0)
818
+ return Decoration.none;
819
+ const ranges = [];
820
+ const tree = syntaxTree(view.state);
821
+ for (const selection of selections) {
822
+ // Selection updates can arrive for every pointermove. Restrict the walk to
823
+ // the selected range so dragging within a short code sample stays O(range)
824
+ // instead of walking an entire long document on every event.
825
+ tree.iterate({
826
+ from: selection.from,
827
+ to: selection.to,
828
+ enter(node) {
829
+ if (node.name !== 'FencedCode')
830
+ return;
831
+ const from = Math.max(node.from, selection.from);
832
+ const to = Math.min(node.to, selection.to);
833
+ if (from < to) {
834
+ ranges.push(Decoration.mark({ class: 'cm-moss-fenced-selection' }).range(from, to));
835
+ }
836
+ return false;
837
+ },
838
+ });
839
+ }
840
+ return Decoration.set(ranges, true);
841
+ }
842
+ const fencedCodeSelectionPlugin = ViewPlugin.fromClass(class {
843
+ constructor(view) {
844
+ Object.defineProperty(this, "decorations", {
845
+ enumerable: true,
846
+ configurable: true,
847
+ writable: true,
848
+ value: void 0
849
+ });
850
+ this.decorations = fencedCodeSelectionDecorations(view);
851
+ }
852
+ update(update) {
853
+ if (update.docChanged || update.selectionSet) {
854
+ this.decorations = fencedCodeSelectionDecorations(update.view);
855
+ }
856
+ }
857
+ }, {
858
+ decorations: (plugin) => plugin.decorations,
859
+ });
860
+ function makeLinkClickHandler(onLinkClick) {
861
+ return EditorView.domEventHandlers({
862
+ click: (event, view) => {
863
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
864
+ return false;
865
+ if (event.button !== 0)
866
+ return false;
867
+ // In read-only mode there's no editable link text to protect, so
868
+ // a click anywhere on the link opens it. In edit mode the open
869
+ // affordance stays scoped to the trailing icon hit-zone so the
870
+ // text itself remains clickable-to-edit.
871
+ const linkEl = view.state.facet(readOnlyFacet)
872
+ ? linkElementFromEvent(event, view.contentDOM)
873
+ : linkIconHitTarget(event, view.contentDOM);
874
+ if (!linkEl)
875
+ return false;
876
+ const pos = view.posAtDOM(linkEl);
877
+ if (pos < 0)
878
+ return false;
879
+ const tree = syntaxTree(view.state);
880
+ let node = tree.resolveInner(pos, 1);
881
+ let visibleUrl = null;
882
+ while (node && node.name !== 'Link') {
883
+ if (node.name === 'URL')
884
+ visibleUrl = node;
885
+ node = node.parent;
886
+ }
887
+ const urlNode = node
888
+ ? linkDestinationUrl(node, view.state.doc)
889
+ : visibleUrl;
890
+ if (!urlNode)
891
+ return false;
892
+ const url = view.state.doc.sliceString(urlNode.from, urlNode.to);
893
+ if (!url)
894
+ return false;
895
+ event.preventDefault();
896
+ event.stopPropagation();
897
+ onLinkClick(url);
898
+ return true;
899
+ },
900
+ });
901
+ }
902
+ /**
903
+ * Assemble the inline-preview extension set. Call once per editor and
904
+ * include the result in your EditorState `extensions` list. Accepts an
905
+ * `onLinkClick` callback so consumers can route link opens through
906
+ * their platform's external-URL mechanism (Tauri IPC, Capacitor
907
+ * browser, etc.) instead of the default `window.open`.
908
+ */
909
+ export function inlinePreview(config = {}) {
910
+ const { onLinkClick = defaultOnLinkClick } = config;
911
+ return [
912
+ taskCheckboxConfigFacet.of(config.taskCheckboxes ?? {}),
913
+ previewActivityExtension(),
914
+ inlinePreviewPlugin,
915
+ fencedCodeSelectionPlugin,
916
+ listEditingExtension(),
917
+ listNavigationExtension(),
918
+ treeProgressPlugin,
919
+ makeLinkClickHandler(onLinkClick),
920
+ ];
921
+ }
922
+ export const mossInlinePreview = inlinePreview;
923
+ export { dedentListItem, indentListItem, insertTightListItem, orderedListRenumberChanges, renumberOrderedLists, } from './list-editing';
924
+ export { defaultOnLinkClick, setFrozen } from './preview-activity';
925
+ //# sourceMappingURL=inline-preview.js.map