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,790 @@
1
+ // File input, upload progress blocks, and upload commands.
2
+ //
3
+ // The document remains plain markdown while an upload is in flight. Pending
4
+ // items live in a CM6 StateField, while File objects, abort controllers, and
5
+ // uploader callbacks live in the runtime maps below.
6
+ import { Facet, Prec, StateEffect, StateField, } from '@codemirror/state';
7
+ import { Decoration, EditorView, ViewPlugin, WidgetType, } from '@codemirror/view';
8
+ import { File as FileIconLucide, RotateCcw, X } from 'lucide-react';
9
+ import { appendMossIcon, mossLucideIcon, } from '../../core/icons';
10
+ import { readOnlyFacet } from '../../core/read-only';
11
+ const DEFAULT_MAX_CONCURRENCY = 3;
12
+ const DEFAULT_UPLOAD_ICONS = {
13
+ file: mossLucideIcon(FileIconLucide, { size: 22 }),
14
+ retry: mossLucideIcon(RotateCcw, { size: 14 }),
15
+ cancel: mossLucideIcon(X, { size: 14 }),
16
+ };
17
+ const uploadIconsFacet = Facet.define({
18
+ combine: (values) => values[0] ?? DEFAULT_UPLOAD_ICONS,
19
+ });
20
+ function resolveUploadIcons(config = {}) {
21
+ return { ...DEFAULT_UPLOAD_ICONS, ...config.icons };
22
+ }
23
+ const runtimeItems = new Map();
24
+ const runtimeBatches = new Map();
25
+ export const uploadEffects = {
26
+ register: StateEffect.define(),
27
+ progress: StateEffect.define(),
28
+ waiting: StateEffect.define(),
29
+ error: StateEffect.define(),
30
+ retry: StateEffect.define(),
31
+ remove: StateEffect.define(),
32
+ };
33
+ function applyEffects(entries, tr) {
34
+ let next = entries;
35
+ for (const effect of tr.effects) {
36
+ if (effect.is(uploadEffects.register)) {
37
+ const registration = effect.value;
38
+ next = [
39
+ ...next,
40
+ {
41
+ ...registration,
42
+ phase: 'uploading',
43
+ progress: 0,
44
+ },
45
+ ];
46
+ }
47
+ else if (effect.is(uploadEffects.progress)) {
48
+ const { id, progress } = effect.value;
49
+ next = next.map((entry) => entry.id === id
50
+ ? { ...entry, progress, phase: 'uploading', error: undefined }
51
+ : entry);
52
+ }
53
+ else if (effect.is(uploadEffects.waiting)) {
54
+ const { id } = effect.value;
55
+ next = next.map((entry) => entry.id === id ? { ...entry, phase: 'waiting' } : entry);
56
+ }
57
+ else if (effect.is(uploadEffects.error)) {
58
+ const { id, error } = effect.value;
59
+ next = next.map((entry) => entry.id === id ? { ...entry, phase: 'error', error } : entry);
60
+ }
61
+ else if (effect.is(uploadEffects.retry)) {
62
+ const { id } = effect.value;
63
+ next = next.map((entry) => entry.id === id
64
+ ? { ...entry, phase: 'uploading', progress: 0, error: undefined }
65
+ : entry);
66
+ }
67
+ else if (effect.is(uploadEffects.remove)) {
68
+ const { id } = effect.value;
69
+ next = next.filter((entry) => entry.id !== id);
70
+ }
71
+ }
72
+ return next;
73
+ }
74
+ const uploadField = StateField.define({
75
+ create: () => [],
76
+ update(entries, tr) {
77
+ if (!tr.docChanged && tr.effects.length === 0)
78
+ return entries;
79
+ const mapped = tr.docChanged
80
+ ? entries.map((entry) => ({
81
+ ...entry,
82
+ // Mapping with assoc=1 keeps pending uploads after text inserted
83
+ // at their target instead of allowing a later upload to overwrite it.
84
+ pos: tr.changes.mapPos(entry.pos, 1),
85
+ to: tr.changes.mapPos(entry.to, 1),
86
+ }))
87
+ : entries;
88
+ return tr.effects.length > 0 ? applyEffects(mapped, tr) : mapped;
89
+ },
90
+ provide: (field) => EditorView.decorations.from(field, buildDecorations),
91
+ });
92
+ function buildDecorations(entries) {
93
+ if (entries.length === 0)
94
+ return Decoration.none;
95
+ const ranges = entries.map((entry) => Decoration.widget({
96
+ widget: new UploadWidget(entry),
97
+ block: true,
98
+ side: 1,
99
+ }).range(entry.pos));
100
+ return Decoration.set(ranges, true);
101
+ }
102
+ class UploadWidget extends WidgetType {
103
+ constructor(entry) {
104
+ super();
105
+ Object.defineProperty(this, "entry", {
106
+ enumerable: true,
107
+ configurable: true,
108
+ writable: true,
109
+ value: entry
110
+ });
111
+ }
112
+ eq(other) {
113
+ const a = this.entry;
114
+ const b = other.entry;
115
+ return (a.id === b.id &&
116
+ a.phase === b.phase &&
117
+ a.progress === b.progress &&
118
+ a.error === b.error);
119
+ }
120
+ toDOM(view) {
121
+ const entry = this.entry;
122
+ const wrap = document.createElement('div');
123
+ wrap.className = 'cm-moss-upload';
124
+ wrap.dataset.id = entry.id;
125
+ const preview = document.createElement('div');
126
+ preview.className = 'cm-moss-upload-preview';
127
+ if (entry.kind === 'image') {
128
+ const image = document.createElement('img');
129
+ image.src = entry.localUrl;
130
+ image.alt = entry.fileName;
131
+ preview.appendChild(image);
132
+ }
133
+ else {
134
+ preview.classList.add('cm-moss-upload-preview-file');
135
+ const glyph = document.createElement('span');
136
+ glyph.className = 'cm-moss-upload-file-glyph';
137
+ appendMossIcon(glyph, view.state.facet(uploadIconsFacet).file);
138
+ const extension = document.createElement('span');
139
+ extension.className = 'cm-moss-upload-ext';
140
+ extension.textContent = extOf(entry.fileName) || 'FILE';
141
+ preview.append(glyph, extension);
142
+ }
143
+ const body = document.createElement('div');
144
+ body.className = 'cm-moss-upload-body';
145
+ const meta = document.createElement('div');
146
+ meta.className = 'cm-moss-upload-meta';
147
+ meta.textContent = `${entry.fileName} · ${formatBytes(entry.fileSize)}`;
148
+ const progress = document.createElement('div');
149
+ progress.className = 'cm-moss-upload-progress';
150
+ const bar = document.createElement('div');
151
+ bar.className = 'cm-moss-upload-bar';
152
+ progress.appendChild(bar);
153
+ const status = document.createElement('div');
154
+ status.className = 'cm-moss-upload-status';
155
+ const actions = document.createElement('div');
156
+ actions.className = 'cm-moss-upload-actions';
157
+ body.append(meta, progress, status, actions);
158
+ wrap.append(preview, body);
159
+ this.paint(wrap, view);
160
+ return wrap;
161
+ }
162
+ updateDOM(dom, view) {
163
+ this.paint(dom, view);
164
+ return true;
165
+ }
166
+ paint(dom, view) {
167
+ const entry = this.entry;
168
+ const bar = dom.querySelector('.cm-moss-upload-bar');
169
+ if (bar) {
170
+ bar.style.width = `${Math.max(0, Math.min(1, entry.progress)) * 100}%`;
171
+ }
172
+ const status = dom.querySelector('.cm-moss-upload-status');
173
+ if (status) {
174
+ const pct = Math.round(entry.progress * 100);
175
+ status.textContent =
176
+ entry.phase === 'error'
177
+ ? `Failed: ${entry.error ?? 'unknown error'}`
178
+ : entry.phase === 'waiting'
179
+ ? 'Waiting for earlier files'
180
+ : `Uploading ${pct}%`;
181
+ status.classList.toggle('is-error', entry.phase === 'error');
182
+ }
183
+ const actions = dom.querySelector('.cm-moss-upload-actions');
184
+ if (!actions)
185
+ return;
186
+ actions.replaceChildren();
187
+ if (entry.phase === 'error') {
188
+ const retry = document.createElement('button');
189
+ retry.type = 'button';
190
+ retry.className = 'cm-moss-upload-btn retry';
191
+ appendMossIcon(retry, view.state.facet(uploadIconsFacet).retry);
192
+ retry.title = 'Retry upload';
193
+ retry.setAttribute('aria-label', 'Retry upload');
194
+ retry.addEventListener('click', (event) => {
195
+ event.preventDefault();
196
+ event.stopPropagation();
197
+ retryUpload(view, entry.id);
198
+ });
199
+ actions.appendChild(retry);
200
+ }
201
+ const cancel = document.createElement('button');
202
+ cancel.type = 'button';
203
+ cancel.className = 'cm-moss-upload-btn cancel';
204
+ appendMossIcon(cancel, view.state.facet(uploadIconsFacet).cancel);
205
+ cancel.title = 'Cancel upload';
206
+ cancel.setAttribute('aria-label', 'Cancel upload');
207
+ cancel.addEventListener('click', (event) => {
208
+ event.preventDefault();
209
+ event.stopPropagation();
210
+ cancelUpload(view, entry.id);
211
+ });
212
+ actions.appendChild(cancel);
213
+ }
214
+ ignoreEvent() {
215
+ return false;
216
+ }
217
+ }
218
+ function uploadBlockExtensions(config = {}) {
219
+ return [
220
+ uploadIconsFacet.of(resolveUploadIcons(config)),
221
+ uploadField,
222
+ uploadCleanupPlugin,
223
+ ];
224
+ }
225
+ export function mossUploadBlocks(config = {}) {
226
+ return uploadBlockExtensions(config);
227
+ }
228
+ export function mossFileUpload(config) {
229
+ return [
230
+ ...uploadBlockExtensions(config),
231
+ Prec.high(EditorView.domEventHandlers({
232
+ paste: (event, view) => {
233
+ if (event.defaultPrevented || !canUpload(view))
234
+ return false;
235
+ if (isInsideTableCell(event.target))
236
+ return false;
237
+ const files = filesFromDataTransfer(event.clipboardData);
238
+ if (files.length === 0)
239
+ return false;
240
+ event.preventDefault();
241
+ const { from, to } = view.state.selection.main;
242
+ beginUploads(view, from, to, files.map((file) => ({ file })), config.uploader, config);
243
+ return true;
244
+ },
245
+ dragover: (event, view) => {
246
+ if (event.defaultPrevented || !canUpload(view))
247
+ return false;
248
+ if (filesFromDataTransfer(event.dataTransfer).length === 0)
249
+ return false;
250
+ event.preventDefault();
251
+ if (event.dataTransfer)
252
+ event.dataTransfer.dropEffect = 'copy';
253
+ return true;
254
+ },
255
+ drop: (event, view) => {
256
+ if (event.defaultPrevented || !canUpload(view))
257
+ return false;
258
+ if (isInsideTableCell(event.target))
259
+ return false;
260
+ const files = filesFromDataTransfer(event.dataTransfer);
261
+ if (files.length === 0)
262
+ return false;
263
+ event.preventDefault();
264
+ const coords = { x: event.clientX, y: event.clientY };
265
+ const pos = view.posAtCoords(coords) ?? view.state.selection.main.from;
266
+ beginUploads(view, pos, pos, files.map((file) => ({ file })), config.uploader, config);
267
+ return true;
268
+ },
269
+ })),
270
+ ];
271
+ }
272
+ export function beginUpload(view, anchorPos, kind, file, uploader) {
273
+ beginUploads(view, anchorPos, anchorPos, [{ file, kind }], uploader);
274
+ }
275
+ export function beginUploads(view, from, to, items, uploader, options = {}) {
276
+ if (!canUpload(view) || items.length === 0)
277
+ return;
278
+ const accepted = selectUploadItems(items, options);
279
+ if (accepted.length === 0)
280
+ return;
281
+ const batchId = genId();
282
+ const start = Math.max(0, Math.min(from, view.state.doc.length));
283
+ const end = Math.max(start, Math.min(to, view.state.doc.length));
284
+ const batch = {
285
+ id: batchId,
286
+ view,
287
+ items: [],
288
+ nextIndex: 0,
289
+ maxConcurrency: normalizeConcurrency(options.maxConcurrency),
290
+ };
291
+ for (const [index, item] of accepted.entries()) {
292
+ const id = genId();
293
+ const localUrl = createLocalUrl(item.file);
294
+ const runtimeItem = {
295
+ id,
296
+ batchId,
297
+ index,
298
+ file: item.file,
299
+ fileName: safeFileName(item.file.name, item.kind),
300
+ uploader,
301
+ kind: item.kind,
302
+ localUrl,
303
+ view,
304
+ controller: new AbortController(),
305
+ lastProgress: 0,
306
+ status: 'pending',
307
+ };
308
+ runtimeItems.set(id, runtimeItem);
309
+ batch.items.push(runtimeItem);
310
+ }
311
+ runtimeBatches.set(batchId, batch);
312
+ view.dispatch({
313
+ effects: batch.items.map((item) => uploadEffects.register.of({
314
+ id: item.id,
315
+ batchId,
316
+ index: item.index,
317
+ pos: start,
318
+ // Only the first item owns the selected range. The remaining items
319
+ // are inserted at the mapped position after the first block.
320
+ to: item.index === 0 ? end : start,
321
+ kind: item.kind,
322
+ fileName: item.fileName,
323
+ fileSize: item.file.size,
324
+ fileType: item.file.type,
325
+ localUrl: item.localUrl,
326
+ })),
327
+ });
328
+ scheduleBatch(batch);
329
+ }
330
+ export function retryUpload(view, id) {
331
+ const item = runtimeItems.get(id);
332
+ if (!item || item.view !== view || item.status !== 'error')
333
+ return;
334
+ const batch = runtimeBatches.get(item.batchId);
335
+ if (!batch || !hasEntry(view, id))
336
+ return;
337
+ item.status = 'pending';
338
+ item.controller = new AbortController();
339
+ item.lastProgress = 0;
340
+ view.dispatch({ effects: uploadEffects.retry.of({ id }) });
341
+ scheduleBatch(batch);
342
+ }
343
+ export function cancelUpload(view, id) {
344
+ const item = runtimeItems.get(id);
345
+ if (!item || item.view !== view)
346
+ return;
347
+ const batch = runtimeBatches.get(item.batchId);
348
+ item.status = 'cancelled';
349
+ item.controller.abort();
350
+ removeRuntimeItem(item);
351
+ if (hasEntry(view, id)) {
352
+ view.dispatch({ effects: uploadEffects.remove.of({ id }) });
353
+ }
354
+ if (batch) {
355
+ flushBatch(batch);
356
+ scheduleBatch(batch);
357
+ }
358
+ }
359
+ export function filesFromDataTransfer(transfer) {
360
+ if (!transfer)
361
+ return [];
362
+ const direct = Array.from(transfer.files ?? []);
363
+ if (direct.length > 0)
364
+ return uniqueFiles(direct);
365
+ const fromItems = [];
366
+ for (const item of Array.from(transfer.items ?? [])) {
367
+ const file = item.getAsFile();
368
+ if (file)
369
+ fromItems.push(file);
370
+ }
371
+ return uniqueFiles(fromItems);
372
+ }
373
+ async function runUpload(item) {
374
+ item.status = 'uploading';
375
+ const onProgress = (ratio) => {
376
+ if (isCancelled(item) || !hasEntry(item.view, item.id))
377
+ return;
378
+ const progress = Math.max(0, Math.min(1, ratio));
379
+ if (Math.abs(progress - item.lastProgress) < 0.02 && progress < 1)
380
+ return;
381
+ item.lastProgress = progress;
382
+ safeDispatch(item.view, {
383
+ effects: uploadEffects.progress.of({ id: item.id, progress }),
384
+ });
385
+ };
386
+ try {
387
+ const result = await item.uploader(item.file, onProgress, item.controller.signal);
388
+ if (isCancelled(item) || !hasEntry(item.view, item.id))
389
+ return;
390
+ assertSafeUrl(result.url);
391
+ item.result = result;
392
+ item.status = 'success';
393
+ safeDispatch(item.view, {
394
+ effects: [
395
+ uploadEffects.progress.of({ id: item.id, progress: 1 }),
396
+ uploadEffects.waiting.of({ id: item.id }),
397
+ ],
398
+ });
399
+ const batch = runtimeBatches.get(item.batchId);
400
+ if (batch) {
401
+ flushBatch(batch);
402
+ scheduleBatch(batch);
403
+ }
404
+ }
405
+ catch (error) {
406
+ if (isCancelled(item))
407
+ return;
408
+ item.status = 'error';
409
+ const message = error instanceof Error ? error.message : String(error);
410
+ if (hasEntry(item.view, item.id)) {
411
+ safeDispatch(item.view, {
412
+ effects: uploadEffects.error.of({ id: item.id, error: message }),
413
+ });
414
+ }
415
+ const batch = runtimeBatches.get(item.batchId);
416
+ if (batch)
417
+ scheduleBatch(batch);
418
+ }
419
+ }
420
+ function flushBatch(batch) {
421
+ if (!isViewUsable(batch.view)) {
422
+ cleanupBatch(batch);
423
+ return;
424
+ }
425
+ const cancelled = [];
426
+ const ready = [];
427
+ while (batch.nextIndex < batch.items.length) {
428
+ const item = batch.items[batch.nextIndex];
429
+ if (item.status === 'cancelled') {
430
+ cancelled.push(item);
431
+ batch.nextIndex++;
432
+ continue;
433
+ }
434
+ if (item.status === 'success') {
435
+ ready.push(item);
436
+ batch.nextIndex++;
437
+ continue;
438
+ }
439
+ break;
440
+ }
441
+ if (cancelled.length > 0 && ready.length === 0) {
442
+ safeDispatch(batch.view, {
443
+ effects: cancelled.map((item) => uploadEffects.remove.of({ id: item.id })),
444
+ });
445
+ for (const item of cancelled)
446
+ removeRuntimeItem(item);
447
+ flushBatch(batch);
448
+ return;
449
+ }
450
+ if (ready.length === 0) {
451
+ cleanupBatch(batch);
452
+ return;
453
+ }
454
+ const firstEntry = readEntry(batch.view, ready[0].id);
455
+ if (!firstEntry) {
456
+ for (const item of [...cancelled, ...ready]) {
457
+ item.status = 'committed';
458
+ removeRuntimeItem(item);
459
+ }
460
+ flushBatch(batch);
461
+ return;
462
+ }
463
+ const markdowns = ready.map((item) => formatMarkdown(item.kind, item.fileName, item.result?.url ?? '', item.result?.kind));
464
+ const insertion = buildBlockInsertion(batch.view, firstEntry.pos, firstEntry.to, markdowns);
465
+ const effects = [
466
+ ...cancelled.map((item) => uploadEffects.remove.of({ id: item.id })),
467
+ ...ready.map((item) => uploadEffects.remove.of({ id: item.id })),
468
+ ];
469
+ try {
470
+ batch.view.dispatch({
471
+ changes: {
472
+ from: firstEntry.pos,
473
+ to: firstEntry.to,
474
+ insert: insertion.text,
475
+ },
476
+ effects,
477
+ selection: { anchor: firstEntry.pos + insertion.caretOffset },
478
+ });
479
+ }
480
+ catch {
481
+ cleanupBatch(batch);
482
+ return;
483
+ }
484
+ for (const item of [...cancelled, ...ready]) {
485
+ item.status = 'committed';
486
+ removeRuntimeItem(item);
487
+ }
488
+ flushBatch(batch);
489
+ }
490
+ function scheduleBatch(batch) {
491
+ if (!runtimeBatches.has(batch.id) || !isViewUsable(batch.view)) {
492
+ cleanupBatch(batch);
493
+ return;
494
+ }
495
+ const active = batch.items.filter((item) => item.status === 'uploading').length;
496
+ const available = Math.max(0, batch.maxConcurrency - active);
497
+ const pending = batch.items.filter((item) => item.status === 'pending');
498
+ for (const item of pending.slice(0, available)) {
499
+ void runUpload(item);
500
+ }
501
+ cleanupBatch(batch);
502
+ }
503
+ function cleanupBatch(batch) {
504
+ const hasLiveItems = batch.items.some((item) => item.status !== 'committed' &&
505
+ item.status !== 'cancelled');
506
+ if (!hasLiveItems)
507
+ runtimeBatches.delete(batch.id);
508
+ }
509
+ function cleanupUploadsForView(view) {
510
+ for (const batch of Array.from(runtimeBatches.values())) {
511
+ if (batch.view !== view)
512
+ continue;
513
+ for (const item of batch.items) {
514
+ item.status = 'cancelled';
515
+ item.controller.abort();
516
+ removeRuntimeItem(item);
517
+ }
518
+ runtimeBatches.delete(batch.id);
519
+ }
520
+ }
521
+ function removeRuntimeItem(item) {
522
+ runtimeItems.delete(item.id);
523
+ revokeLocalUrl(item.localUrl);
524
+ }
525
+ const uploadCleanupPlugin = ViewPlugin.fromClass(class {
526
+ constructor(view) {
527
+ Object.defineProperty(this, "view", {
528
+ enumerable: true,
529
+ configurable: true,
530
+ writable: true,
531
+ value: view
532
+ });
533
+ }
534
+ destroy() {
535
+ cleanupUploadsForView(this.view);
536
+ }
537
+ });
538
+ function canUpload(view) {
539
+ return !view.state.facet(readOnlyFacet) && !view.state.readOnly;
540
+ }
541
+ function isInsideTableCell(target) {
542
+ return target instanceof HTMLElement &&
543
+ Boolean(target.closest('.cm-moss-table-cell, .cm-moss-table-cell-source'));
544
+ }
545
+ function safeDispatch(view, spec) {
546
+ if (isViewUsable(view))
547
+ view.dispatch(spec);
548
+ }
549
+ function isViewUsable(view) {
550
+ return view.dom.isConnected;
551
+ }
552
+ function hasEntry(view, id) {
553
+ const entries = view.state.field(uploadField, false);
554
+ return Array.isArray(entries) && entries.some((entry) => entry.id === id);
555
+ }
556
+ function readEntry(view, id) {
557
+ const entries = view.state.field(uploadField, false);
558
+ return Array.isArray(entries)
559
+ ? entries.find((entry) => entry.id === id)
560
+ : undefined;
561
+ }
562
+ function buildBlockInsertion(view, from, to, markdowns) {
563
+ const doc = view.state.doc;
564
+ const fromLine = doc.lineAt(from);
565
+ const toLine = doc.lineAt(to);
566
+ const before = doc.sliceString(fromLine.from, from);
567
+ const after = doc.sliceString(to, toLine.to);
568
+ const prefix = before.trim() ? '\n\n' : '';
569
+ // Leave a real editable line after an upload at the end of a document.
570
+ // Without it, standalone image/file widgets occupy the visual area below
571
+ // the hidden source line and the caret remains inside that source range.
572
+ const suffix = after.trim() ? '\n\n' : '\n';
573
+ const middle = markdowns.join('\n\n');
574
+ const text = prefix + middle + suffix;
575
+ return { text, caretOffset: text.length };
576
+ }
577
+ function formatMarkdown(kind, fileName, url, resultKind) {
578
+ assertSafeUrl(url);
579
+ const resolvedKind = resultKind ?? kind;
580
+ return resolvedKind === 'file'
581
+ ? `[${fileName}](${url})`
582
+ : `![${fileName}](${url})`;
583
+ }
584
+ function assertSafeUrl(url) {
585
+ if (!url.trim() || /[\s"')]/.test(url)) {
586
+ throw new Error('Uploader returned an invalid URL');
587
+ }
588
+ }
589
+ function selectUploadItems(items, options) {
590
+ const accepted = [];
591
+ const rejected = new Map();
592
+ const report = (file, reason) => {
593
+ const files = rejected.get(reason) ?? [];
594
+ files.push(file);
595
+ rejected.set(reason, files);
596
+ };
597
+ const maxFiles = normalizeLimit(options.maxFiles);
598
+ const maxFileSize = normalizeLimit(options.maxFileSize);
599
+ const maxTotalSize = normalizeLimit(options.maxTotalSize);
600
+ const accepts = normalizeAccept(options.accept);
601
+ let totalSize = 0;
602
+ for (const item of items) {
603
+ const file = item.file;
604
+ if (maxFiles !== undefined && accepted.length >= maxFiles) {
605
+ report(file, 'max-files');
606
+ continue;
607
+ }
608
+ if (maxFileSize !== undefined && file.size > maxFileSize) {
609
+ report(file, 'max-file-size');
610
+ continue;
611
+ }
612
+ if (accepts.length > 0 && !matchesAccept(file, accepts)) {
613
+ report(file, 'accept');
614
+ continue;
615
+ }
616
+ if (maxTotalSize !== undefined && totalSize + file.size > maxTotalSize) {
617
+ report(file, 'max-total-size');
618
+ continue;
619
+ }
620
+ totalSize += file.size;
621
+ accepted.push({
622
+ file,
623
+ kind: item.kind ?? options.resolveKind?.(file) ?? defaultKind(file),
624
+ });
625
+ }
626
+ for (const [reason, files] of rejected) {
627
+ options.onRejected?.(files, reason);
628
+ }
629
+ return accepted;
630
+ }
631
+ function normalizeConcurrency(value) {
632
+ if (value === undefined || !Number.isFinite(value)) {
633
+ return DEFAULT_MAX_CONCURRENCY;
634
+ }
635
+ return Math.max(1, Math.floor(value));
636
+ }
637
+ function normalizeLimit(value) {
638
+ if (value === undefined || !Number.isFinite(value) || value < 0)
639
+ return undefined;
640
+ return value;
641
+ }
642
+ function normalizeAccept(accept) {
643
+ if (!accept)
644
+ return [];
645
+ const values = typeof accept === 'string' ? accept.split(',') : accept;
646
+ return values
647
+ .map((item) => item.trim().toLowerCase())
648
+ .filter(Boolean);
649
+ }
650
+ function isCancelled(item) {
651
+ return item.status === 'cancelled';
652
+ }
653
+ function matchesAccept(file, accepts) {
654
+ const type = file.type.toLowerCase();
655
+ const name = file.name.toLowerCase();
656
+ return accepts.some((accept) => {
657
+ if (accept.startsWith('.'))
658
+ return name.endsWith(accept);
659
+ if (accept.endsWith('/*'))
660
+ return type.startsWith(accept.slice(0, -1));
661
+ return type === accept;
662
+ });
663
+ }
664
+ function defaultKind(file) {
665
+ return file.type.toLowerCase().startsWith('image/') ? 'image' : 'file';
666
+ }
667
+ function safeFileName(name, kind) {
668
+ const cleaned = name
669
+ .replace(/[\[\]|\\\r\n]/g, '_')
670
+ .replace(/\s+/g, ' ')
671
+ .trim();
672
+ return cleaned || (kind === 'image' ? 'image' : 'file');
673
+ }
674
+ function uniqueFiles(files) {
675
+ const seen = new Set();
676
+ const result = [];
677
+ for (const file of files) {
678
+ if (seen.has(file))
679
+ continue;
680
+ seen.add(file);
681
+ result.push(file);
682
+ }
683
+ return result;
684
+ }
685
+ function createLocalUrl(file) {
686
+ return typeof URL.createObjectURL === 'function'
687
+ ? URL.createObjectURL(file)
688
+ : '';
689
+ }
690
+ function revokeLocalUrl(url) {
691
+ if (url && typeof URL.revokeObjectURL === 'function')
692
+ URL.revokeObjectURL(url);
693
+ }
694
+ function genId() {
695
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
696
+ return crypto.randomUUID();
697
+ }
698
+ return `up_${Date.now()}_${Math.random().toString(36).slice(2)}`;
699
+ }
700
+ function formatBytes(bytes) {
701
+ if (bytes < 1024)
702
+ return `${bytes} B`;
703
+ if (bytes < 1024 * 1024)
704
+ return `${(bytes / 1024).toFixed(1)} KB`;
705
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
706
+ }
707
+ function extOf(fileName) {
708
+ const index = fileName.lastIndexOf('.');
709
+ return index >= 0 ? fileName.slice(index + 1).toUpperCase() : '';
710
+ }
711
+ export function mossUploadCommands(uploader, options = {}) {
712
+ return [
713
+ {
714
+ id: 'upload-image',
715
+ label: 'Upload image',
716
+ detail: 'Pick from disk, upload, insert ![name](url)',
717
+ keywords: ['picture', 'photo', 'image', 'img'],
718
+ icon: 'image',
719
+ apply: async (view, from) => {
720
+ const files = await pickFiles('image/*');
721
+ if (files.length === 0)
722
+ return;
723
+ const line = view.state.doc.lineAt(from);
724
+ clearTriggerLine(view, line);
725
+ beginUploads(view, line.from, line.from, files.map((file) => ({ file, kind: 'image' })), uploader, options);
726
+ },
727
+ },
728
+ {
729
+ id: 'upload-file',
730
+ label: 'Upload file',
731
+ detail: 'Pick from disk, upload, insert [name](url)',
732
+ keywords: ['attachment', 'file', 'link'],
733
+ icon: 'file',
734
+ apply: async (view, from) => {
735
+ const files = await pickFiles();
736
+ if (files.length === 0)
737
+ return;
738
+ const line = view.state.doc.lineAt(from);
739
+ clearTriggerLine(view, line);
740
+ beginUploads(view, line.from, line.from, files.map((file) => ({ file, kind: 'file' })), uploader, options);
741
+ },
742
+ },
743
+ ];
744
+ }
745
+ function clearTriggerLine(view, line) {
746
+ if (line.text.trim() === '')
747
+ return;
748
+ view.dispatch({
749
+ changes: { from: line.from, to: line.to, insert: '' },
750
+ selection: { anchor: line.from },
751
+ userEvent: 'input',
752
+ });
753
+ }
754
+ function pickFiles(accept) {
755
+ return new Promise((resolve) => {
756
+ const input = document.createElement('input');
757
+ input.type = 'file';
758
+ input.multiple = true;
759
+ if (accept)
760
+ input.accept = accept;
761
+ input.style.position = 'fixed';
762
+ input.style.top = '-9999px';
763
+ input.style.opacity = '0';
764
+ let settled = false;
765
+ const cleanup = () => {
766
+ if (settled)
767
+ return;
768
+ settled = true;
769
+ input.remove();
770
+ window.removeEventListener('focus', onFocus, true);
771
+ };
772
+ const onFocus = () => {
773
+ window.setTimeout(() => {
774
+ if (!settled && !input.files?.length) {
775
+ cleanup();
776
+ resolve([]);
777
+ }
778
+ }, 300);
779
+ };
780
+ input.addEventListener('change', () => {
781
+ const files = Array.from(input.files ?? []);
782
+ cleanup();
783
+ resolve(files);
784
+ });
785
+ window.addEventListener('focus', onFocus, true);
786
+ document.body.appendChild(input);
787
+ input.click();
788
+ });
789
+ }
790
+ //# sourceMappingURL=index.js.map