@wonderwhy-er/desktop-commander 0.2.39 → 0.2.40

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 (137) hide show
  1. package/dist/server.js +1 -1
  2. package/dist/ui/file-preview/preview-runtime.js +204 -153
  3. package/dist/ui/file-preview/src/markdown/controller.d.ts +7 -1
  4. package/dist/ui/file-preview/src/markdown/controller.js +135 -16
  5. package/dist/ui/file-preview/src/markdown/editor.d.ts +97 -1
  6. package/dist/ui/file-preview/src/markdown/editor.js +814 -26
  7. package/dist/ui/file-preview/src/model.d.ts +2 -1
  8. package/dist/utils/capture.js +1 -1
  9. package/dist/utils/toolHistory.d.ts +13 -0
  10. package/dist/utils/toolHistory.js +65 -0
  11. package/dist/version.d.ts +1 -1
  12. package/dist/version.js +1 -1
  13. package/package.json +7 -1
  14. package/dist/ui/config-editor/app.js +0 -840
  15. package/dist/ui/config-editor/array-modal.d.ts +0 -19
  16. package/dist/ui/config-editor/array-modal.js +0 -185
  17. package/dist/ui/config-editor/main.d.ts +0 -1
  18. package/dist/ui/config-editor/main.js +0 -2
  19. package/dist/ui/config-editor/src/App.d.ts +0 -43
  20. package/dist/ui/config-editor/src/components/layout.d.ts +0 -4
  21. package/dist/ui/config-editor/src/components/layout.js +0 -83
  22. package/dist/ui/config-editor/src/components/toolbar.d.ts +0 -1
  23. package/dist/ui/config-editor/src/components/toolbar.js +0 -21
  24. package/dist/ui/config-editor/src/config-values.d.ts +0 -6
  25. package/dist/ui/config-editor/src/config-values.js +0 -61
  26. package/dist/ui/config-editor/src/contracts.d.ts +0 -14
  27. package/dist/ui/config-editor/src/contracts.js +0 -3
  28. package/dist/ui/config-editor/src/directory-browser.d.ts +0 -6
  29. package/dist/ui/config-editor/src/directory-browser.js +0 -71
  30. package/dist/ui/config-editor/src/layout.d.ts +0 -5
  31. package/dist/ui/config-editor/src/layout.js +0 -90
  32. package/dist/ui/config-editor/src/parsing.d.ts +0 -5
  33. package/dist/ui/config-editor/src/parsing.js +0 -50
  34. package/dist/ui/config-editor/src/toolbar.d.ts +0 -1
  35. package/dist/ui/config-editor/src/toolbar.js +0 -18
  36. package/dist/ui/config-editor/src/types.d.ts +0 -17
  37. package/dist/ui/config-editor/src/types.js +0 -3
  38. package/dist/ui/config-editor/src/utils/config-values.d.ts +0 -9
  39. package/dist/ui/config-editor/src/utils/config-values.js +0 -61
  40. package/dist/ui/config-editor/src/utils/directory-browser.d.ts +0 -31
  41. package/dist/ui/config-editor/src/utils/directory-browser.js +0 -201
  42. package/dist/ui/config-editor/src/utils/parsing.d.ts +0 -8
  43. package/dist/ui/config-editor/src/utils/parsing.js +0 -50
  44. package/dist/ui/file-preview/app.d.ts +0 -8
  45. package/dist/ui/file-preview/app.js +0 -2020
  46. package/dist/ui/file-preview/components/code-viewer.d.ts +0 -6
  47. package/dist/ui/file-preview/components/code-viewer.js +0 -73
  48. package/dist/ui/file-preview/components/highlighting.d.ts +0 -2
  49. package/dist/ui/file-preview/components/highlighting.js +0 -54
  50. package/dist/ui/file-preview/components/html-renderer.d.ts +0 -5
  51. package/dist/ui/file-preview/components/html-renderer.js +0 -47
  52. package/dist/ui/file-preview/components/markdown-renderer.d.ts +0 -1
  53. package/dist/ui/file-preview/components/markdown-renderer.js +0 -67
  54. package/dist/ui/file-preview/components/toolbar.d.ts +0 -6
  55. package/dist/ui/file-preview/components/toolbar.js +0 -75
  56. package/dist/ui/file-preview/image-preview.d.ts +0 -3
  57. package/dist/ui/file-preview/image-preview.js +0 -21
  58. package/dist/ui/file-preview/main.d.ts +0 -1
  59. package/dist/ui/file-preview/main.js +0 -5
  60. package/dist/ui/file-preview/markdown/editor.d.ts +0 -36
  61. package/dist/ui/file-preview/markdown/editor.js +0 -643
  62. package/dist/ui/file-preview/markdown/linking.d.ts +0 -9
  63. package/dist/ui/file-preview/markdown/linking.js +0 -210
  64. package/dist/ui/file-preview/markdown/outline.d.ts +0 -7
  65. package/dist/ui/file-preview/markdown/outline.js +0 -40
  66. package/dist/ui/file-preview/markdown/preview.d.ts +0 -8
  67. package/dist/ui/file-preview/markdown/preview.js +0 -33
  68. package/dist/ui/file-preview/markdown/slugify.d.ts +0 -3
  69. package/dist/ui/file-preview/markdown/slugify.js +0 -31
  70. package/dist/ui/file-preview/markdown/toc.d.ts +0 -11
  71. package/dist/ui/file-preview/markdown/toc.js +0 -75
  72. package/dist/ui/file-preview/markdown/utils.d.ts +0 -1
  73. package/dist/ui/file-preview/markdown/utils.js +0 -15
  74. package/dist/ui/file-preview/markdown/workspace-controller.d.ts +0 -25
  75. package/dist/ui/file-preview/markdown/workspace-controller.js +0 -40
  76. package/dist/ui/file-preview/src/components/CodeViewer.d.ts +0 -6
  77. package/dist/ui/file-preview/src/components/CodeViewer.js +0 -60
  78. package/dist/ui/file-preview/src/components/HtmlRenderer.d.ts +0 -8
  79. package/dist/ui/file-preview/src/components/HtmlRenderer.js +0 -45
  80. package/dist/ui/file-preview/src/components/MarkdownRenderer.d.ts +0 -1
  81. package/dist/ui/file-preview/src/components/MarkdownRenderer.js +0 -15
  82. package/dist/ui/file-preview/src/components/Toolbar.d.ts +0 -6
  83. package/dist/ui/file-preview/src/components/Toolbar.js +0 -75
  84. package/dist/ui/file-preview/src/components/editor-toolbar.d.ts +0 -15
  85. package/dist/ui/file-preview/src/components/editor-toolbar.js +0 -384
  86. package/dist/ui/file-preview/src/components/markdown-editor.d.ts +0 -29
  87. package/dist/ui/file-preview/src/components/markdown-editor.js +0 -535
  88. package/dist/ui/file-preview/src/markdown/block-merge.d.ts +0 -25
  89. package/dist/ui/file-preview/src/markdown/block-merge.js +0 -86
  90. package/dist/ui/file-preview/src/markdown/link-modal.d.ts +0 -13
  91. package/dist/ui/file-preview/src/markdown/link-modal.js +0 -213
  92. package/dist/ui/file-preview/src/markdown/raw-editor.d.ts +0 -8
  93. package/dist/ui/file-preview/src/markdown/raw-editor.js +0 -61
  94. package/dist/ui/file-preview/src/markdown/selection-toolbar.d.ts +0 -14
  95. package/dist/ui/file-preview/src/markdown/selection-toolbar.js +0 -128
  96. package/dist/ui/file-preview/src/markdown/toc.d.ts +0 -11
  97. package/dist/ui/file-preview/src/markdown/toc.js +0 -75
  98. package/dist/ui/file-preview/src/markdown-workspace/editor.d.ts +0 -36
  99. package/dist/ui/file-preview/src/markdown-workspace/editor.js +0 -643
  100. package/dist/ui/file-preview/src/markdown-workspace/linking.d.ts +0 -9
  101. package/dist/ui/file-preview/src/markdown-workspace/linking.js +0 -210
  102. package/dist/ui/file-preview/src/markdown-workspace/outline.d.ts +0 -7
  103. package/dist/ui/file-preview/src/markdown-workspace/outline.js +0 -40
  104. package/dist/ui/file-preview/src/markdown-workspace/preview.d.ts +0 -8
  105. package/dist/ui/file-preview/src/markdown-workspace/preview.js +0 -33
  106. package/dist/ui/file-preview/src/markdown-workspace/slugify.d.ts +0 -3
  107. package/dist/ui/file-preview/src/markdown-workspace/slugify.js +0 -31
  108. package/dist/ui/file-preview/src/markdown-workspace/toc.d.ts +0 -11
  109. package/dist/ui/file-preview/src/markdown-workspace/toc.js +0 -75
  110. package/dist/ui/file-preview/src/markdown-workspace/utils.d.ts +0 -1
  111. package/dist/ui/file-preview/src/markdown-workspace/utils.js +0 -15
  112. package/dist/ui/file-preview/src/markdown-workspace/workspace-controller.d.ts +0 -25
  113. package/dist/ui/file-preview/src/markdown-workspace/workspace-controller.js +0 -40
  114. package/dist/ui/file-preview/types.d.ts +0 -1
  115. package/dist/ui/file-preview/types.js +0 -1
  116. package/dist/ui/server-integration.d.ts +0 -13
  117. package/dist/ui/server-integration.js +0 -31
  118. package/dist/ui/shared/ToolHeader.d.ts +0 -9
  119. package/dist/ui/shared/ToolHeader.js +0 -29
  120. package/dist/ui/shared/app-bootstrap.d.ts +0 -9
  121. package/dist/ui/shared/app-bootstrap.js +0 -15
  122. package/dist/ui/shared/guards.d.ts +0 -1
  123. package/dist/ui/shared/guards.js +0 -3
  124. package/dist/ui/shared/host-lifecycle.d.ts +0 -17
  125. package/dist/ui/shared/host-lifecycle.js +0 -41
  126. package/dist/ui/shared/rpc-client.d.ts +0 -14
  127. package/dist/ui/shared/rpc-client.js +0 -72
  128. package/dist/ui/shared/theme-adaptation.d.ts +0 -10
  129. package/dist/ui/shared/theme-adaptation.js +0 -118
  130. package/dist/ui/shared/tool-header.d.ts +0 -9
  131. package/dist/ui/shared/tool-header.js +0 -25
  132. package/dist/utils/ui-call-context.d.ts +0 -8
  133. package/dist/utils/ui-call-context.js +0 -72
  134. /package/dist/ui/config-editor/{app.d.ts → src/app.d.ts} +0 -0
  135. /package/dist/ui/config-editor/src/{App.js → app.js} +0 -0
  136. /package/dist/ui/file-preview/src/{App.d.ts → app.d.ts} +0 -0
  137. /package/dist/ui/file-preview/src/{App.js → app.js} +0 -0
@@ -1,535 +0,0 @@
1
- /**
2
- * WYSIWYG markdown editor for the file preview. Makes rendered markdown
3
- * blocks contentEditable and translates user changes back to markdown
4
- * source, calling edit_block for surgical file updates.
5
- */
6
- import { renderMarkdown } from './markdown-renderer.js';
7
- import { createEditorToolbar, handleFormatKeydown } from './editor-toolbar.js';
8
- // ---------------------------------------------------------------------------
9
- // Markdown source → blocks parser
10
- // ---------------------------------------------------------------------------
11
- export function parseMarkdownBlocks(source) {
12
- const lines = source.split('\n');
13
- const blocks = [];
14
- let i = 0;
15
- while (i < lines.length) {
16
- const line = lines[i];
17
- // Blank line — skip
18
- if (line.trim() === '') {
19
- i++;
20
- continue;
21
- }
22
- // Fenced code block (``` or ~~~)
23
- const fenceMatch = line.match(/^(`{3,}|~{3,})/);
24
- if (fenceMatch) {
25
- const fence = fenceMatch[1];
26
- const start = i;
27
- i++;
28
- while (i < lines.length && !lines[i].startsWith(fence))
29
- i++;
30
- if (i < lines.length)
31
- i++; // closing fence
32
- blocks.push({
33
- type: 'code',
34
- source: lines.slice(start, i).join('\n'),
35
- startLine: start + 1,
36
- endLine: i,
37
- editable: false,
38
- });
39
- continue;
40
- }
41
- // Setext heading (underline with = or -)
42
- if (i + 1 < lines.length &&
43
- /^[=-]+\s*$/.test(lines[i + 1]) &&
44
- line.trim() !== '') {
45
- const level = lines[i + 1].startsWith('=') ? 1 : 2;
46
- blocks.push({
47
- type: 'heading',
48
- source: lines.slice(i, i + 2).join('\n'),
49
- startLine: i + 1,
50
- endLine: i + 2,
51
- level,
52
- editable: true,
53
- });
54
- i += 2;
55
- continue;
56
- }
57
- // ATX heading
58
- const headingMatch = line.match(/^(#{1,6})\s+(.*)/);
59
- if (headingMatch) {
60
- blocks.push({
61
- type: 'heading',
62
- source: line,
63
- startLine: i + 1,
64
- endLine: i + 1,
65
- level: headingMatch[1].length,
66
- editable: true,
67
- });
68
- i++;
69
- continue;
70
- }
71
- // Horizontal rule
72
- if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line)) {
73
- blocks.push({
74
- type: 'hr',
75
- source: line,
76
- startLine: i + 1,
77
- endLine: i + 1,
78
- editable: false,
79
- });
80
- i++;
81
- continue;
82
- }
83
- // Blockquote
84
- if (line.startsWith('>')) {
85
- const start = i;
86
- while (i < lines.length &&
87
- (lines[i].startsWith('>') ||
88
- (lines[i].trim() !== '' && !isBlockStart(lines[i]) && i > start))) {
89
- i++;
90
- if (i < lines.length && lines[i].trim() === '')
91
- break;
92
- }
93
- blocks.push({
94
- type: 'blockquote',
95
- source: lines.slice(start, i).join('\n'),
96
- startLine: start + 1,
97
- endLine: i,
98
- editable: true,
99
- });
100
- continue;
101
- }
102
- // List (unordered or ordered)
103
- const listMatch = line.match(/^(\s*)([-*+]|\d+\.)\s/);
104
- if (listMatch) {
105
- const start = i;
106
- const isOrdered = /\d+\./.test(listMatch[2]);
107
- i++;
108
- while (i < lines.length) {
109
- const l = lines[i];
110
- if (l.trim() === '') {
111
- // Blank inside list — check if next line continues the list
112
- if (i + 1 < lines.length && /^(\s*)([-*+]|\d+\.)\s/.test(lines[i + 1])) {
113
- i++;
114
- continue;
115
- }
116
- break;
117
- }
118
- if (/^(\s*)([-*+]|\d+\.)\s/.test(l) || /^\s+\S/.test(l)) {
119
- i++;
120
- }
121
- else {
122
- break;
123
- }
124
- }
125
- blocks.push({
126
- type: 'list',
127
- source: lines.slice(start, i).join('\n'),
128
- startLine: start + 1,
129
- endLine: i,
130
- listType: isOrdered ? 'ol' : 'ul',
131
- editable: true,
132
- });
133
- continue;
134
- }
135
- // Table (pipe-delimited with separator row)
136
- if (line.includes('|') &&
137
- i + 1 < lines.length &&
138
- /^\|?\s*[-:]+[-| :]*$/.test(lines[i + 1])) {
139
- const start = i;
140
- while (i < lines.length && lines[i].includes('|') && lines[i].trim() !== '')
141
- i++;
142
- blocks.push({
143
- type: 'table',
144
- source: lines.slice(start, i).join('\n'),
145
- startLine: start + 1,
146
- endLine: i,
147
- editable: false,
148
- });
149
- continue;
150
- }
151
- // Paragraph (default — consecutive non-blank non-block-start lines)
152
- {
153
- const start = i;
154
- i++;
155
- while (i < lines.length && lines[i].trim() !== '' && !isBlockStart(lines[i])) {
156
- i++;
157
- }
158
- blocks.push({
159
- type: 'paragraph',
160
- source: lines.slice(start, i).join('\n'),
161
- startLine: start + 1,
162
- endLine: i,
163
- editable: true,
164
- });
165
- }
166
- }
167
- return blocks;
168
- }
169
- /** Returns true if a line starts a new block (heading, fence, hr, list, quote, table). */
170
- function isBlockStart(line) {
171
- if (/^(#{1,6})\s/.test(line))
172
- return true;
173
- if (/^(`{3,}|~{3,})/.test(line))
174
- return true;
175
- if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line))
176
- return true;
177
- if (line.startsWith('>'))
178
- return true;
179
- if (/^(\s*)([-*+]|\d+\.)\s/.test(line))
180
- return true;
181
- return false;
182
- }
183
- // ---------------------------------------------------------------------------
184
- // HTML → inline markdown conversion (for contentEditable round-tripping)
185
- // ---------------------------------------------------------------------------
186
- function nodeToMarkdown(node) {
187
- if (node.nodeType === Node.TEXT_NODE) {
188
- return node.textContent ?? '';
189
- }
190
- if (node.nodeType !== Node.ELEMENT_NODE)
191
- return '';
192
- const el = node;
193
- const tag = el.tagName.toLowerCase();
194
- const children = Array.from(el.childNodes).map(nodeToMarkdown).join('');
195
- switch (tag) {
196
- case 'strong':
197
- case 'b':
198
- return `**${children}**`;
199
- case 'em':
200
- case 'i':
201
- return `*${children}*`;
202
- case 'code':
203
- // Inline code only (not inside <pre>)
204
- if (!el.closest('pre'))
205
- return `\`${el.textContent ?? ''}\``;
206
- return children;
207
- case 'a': {
208
- const href = el.getAttribute('href') ?? '';
209
- return `[${children}](${href})`;
210
- }
211
- case 'img': {
212
- const alt = el.getAttribute('alt') ?? '';
213
- const src = el.getAttribute('src') ?? '';
214
- return `![${alt}](${src})`;
215
- }
216
- case 'br':
217
- return '\n';
218
- case 'del':
219
- case 's':
220
- case 'strike':
221
- return `~~${children}~~`;
222
- case 'span': {
223
- // Preserve styled spans as inline HTML (color, font-size)
224
- const style = el.getAttribute('style') ?? '';
225
- if (style) {
226
- return `<span style="${style}">${children}</span>`;
227
- }
228
- return children;
229
- }
230
- case 'font': {
231
- // execCommand('foreColor') produces <font color="...">
232
- // execCommand('fontSize') produces <font size="...">
233
- const color = el.getAttribute('color');
234
- const size = el.getAttribute('size');
235
- if (color) {
236
- return `<span style="color: ${color}">${children}</span>`;
237
- }
238
- if (size) {
239
- return `<span style="font-size: ${size}">${children}</span>`;
240
- }
241
- return children;
242
- }
243
- default:
244
- return children;
245
- }
246
- }
247
- function extractInlineMarkdown(element) {
248
- return Array.from(element.childNodes).map(nodeToMarkdown).join('').trim();
249
- }
250
- // ---------------------------------------------------------------------------
251
- // Block → markdown reconstruction from edited DOM
252
- // ---------------------------------------------------------------------------
253
- function blockToMarkdown(block, editedEl) {
254
- switch (block.type) {
255
- case 'heading': {
256
- const prefix = '#'.repeat(block.level ?? 1);
257
- const text = extractInlineMarkdown(editedEl);
258
- return `${prefix} ${text}`;
259
- }
260
- case 'paragraph': {
261
- const pTags = editedEl.querySelectorAll('p');
262
- if (pTags.length > 0) {
263
- return Array.from(pTags)
264
- .map(p => extractInlineMarkdown(p))
265
- .join('\n\n');
266
- }
267
- return extractInlineMarkdown(editedEl);
268
- }
269
- case 'list': {
270
- const listItems = editedEl.querySelectorAll(':scope > li');
271
- const isOrdered = block.listType === 'ol';
272
- return Array.from(listItems)
273
- .map((li, idx) => {
274
- const liEl = li;
275
- // markdown-it wraps loose list content in <p>
276
- const pEl = liEl.querySelector(':scope > p');
277
- const text = extractInlineMarkdown(pEl ?? liEl);
278
- const prefix = isOrdered ? `${idx + 1}. ` : '- ';
279
- return `${prefix}${text}`;
280
- })
281
- .join('\n');
282
- }
283
- case 'blockquote': {
284
- const paragraphs = editedEl.querySelectorAll('p');
285
- let text;
286
- if (paragraphs.length > 0) {
287
- text = Array.from(paragraphs)
288
- .map(p => extractInlineMarkdown(p))
289
- .join('\n\n');
290
- }
291
- else {
292
- text = extractInlineMarkdown(editedEl);
293
- }
294
- return text
295
- .split('\n')
296
- .map(line => `> ${line}`)
297
- .join('\n');
298
- }
299
- default:
300
- return block.source;
301
- }
302
- }
303
- // ---------------------------------------------------------------------------
304
- // Editable rendering — wraps each block in a container with data attributes
305
- // ---------------------------------------------------------------------------
306
- export function renderEditableMarkdown(source) {
307
- const blocks = parseMarkdownBlocks(source);
308
- let html = '';
309
- for (const block of blocks) {
310
- const rendered = renderMarkdown(block.source);
311
- const editableClass = block.editable ? ' md-block--editable' : '';
312
- const attrs = [
313
- `data-md-start="${block.startLine}"`,
314
- `data-md-end="${block.endLine}"`,
315
- `data-md-type="${block.type}"`,
316
- block.level !== undefined ? `data-md-level="${block.level}"` : '',
317
- block.listType ? `data-md-list="${block.listType}"` : '',
318
- ]
319
- .filter(Boolean)
320
- .join(' ');
321
- html += `<div class="md-block${editableClass}" ${attrs}>${rendered}</div>`;
322
- }
323
- return html;
324
- }
325
- // ---------------------------------------------------------------------------
326
- // Editor handlers — attaches click/blur/keyboard editing to the DOM
327
- // ---------------------------------------------------------------------------
328
- /**
329
- * Find the inner content element that should become contentEditable.
330
- * Returns null for non-editable block types.
331
- */
332
- function getEditableChild(blockEl) {
333
- const type = blockEl.dataset.mdType;
334
- switch (type) {
335
- case 'heading':
336
- return blockEl.querySelector('h1, h2, h3, h4, h5, h6');
337
- case 'paragraph':
338
- return blockEl.querySelector('p') ?? blockEl;
339
- case 'list':
340
- return blockEl.querySelector('ul, ol');
341
- case 'blockquote':
342
- return blockEl.querySelector('blockquote');
343
- default:
344
- return null;
345
- }
346
- }
347
- function truncate(text, maxLen) {
348
- const singleLine = text.replace(/\n/g, ' ').trim();
349
- return singleLine.length > maxLen ? singleLine.slice(0, maxLen) + '...' : singleLine;
350
- }
351
- /**
352
- * Attach WYSIWYG editing handlers to editable markdown blocks inside
353
- * the given container. All editable blocks are contentEditable from the
354
- * start — users can select and type immediately without an activation
355
- * click. Changes are committed when focus leaves the editor.
356
- */
357
- export function attachEditorHandlers(container, source, filePath, callbacks) {
358
- const blocks = parseMarkdownBlocks(source);
359
- const blockMap = new Map();
360
- for (const block of blocks) {
361
- blockMap.set(`${block.startLine}-${block.endLine}`, block);
362
- }
363
- // Snapshot each block's original innerHTML so we can detect changes
364
- const originalHTML = new Map();
365
- let activeBlockEl = null;
366
- let activeEditEl = null;
367
- const editHistory = [];
368
- // Floating formatting toolbar
369
- const toolbar = createEditorToolbar();
370
- container.style.position = 'relative';
371
- container.appendChild(toolbar.element);
372
- // --- Make all editable blocks live from the start ---
373
- const editableBlocks = container.querySelectorAll('.md-block--editable');
374
- editableBlocks.forEach(blockEl => {
375
- const editEl = getEditableChild(blockEl);
376
- if (editEl) {
377
- editEl.contentEditable = 'true';
378
- const key = blockKey(blockEl);
379
- originalHTML.set(key, editEl.innerHTML);
380
- }
381
- });
382
- function blockKey(el) {
383
- return `${el.dataset.mdStart}-${el.dataset.mdEnd}`;
384
- }
385
- async function commitBlock(blockEl, editEl) {
386
- const key = blockKey(blockEl);
387
- const block = blockMap.get(key);
388
- if (!block)
389
- return;
390
- const origHTML = originalHTML.get(key);
391
- if (editEl.innerHTML === origHTML)
392
- return; // No change
393
- const newMarkdown = blockToMarkdown(block, editEl);
394
- if (newMarkdown === block.source)
395
- return;
396
- callbacks.trackEvent?.('md_block_edited', {
397
- block_type: block.type,
398
- line: block.startLine,
399
- });
400
- const prevSource = block.source;
401
- try {
402
- await callbacks.callTool('edit_block', {
403
- file_path: filePath,
404
- old_string: block.source,
405
- new_string: newMarkdown,
406
- });
407
- block.source = newMarkdown;
408
- editHistory.push({
409
- oldText: prevSource,
410
- newText: newMarkdown,
411
- line: block.startLine,
412
- });
413
- // Re-render the block and make it editable again
414
- const freshHtml = renderMarkdown(newMarkdown);
415
- blockEl.innerHTML = freshHtml;
416
- const newEditEl = getEditableChild(blockEl);
417
- if (newEditEl) {
418
- newEditEl.contentEditable = 'true';
419
- originalHTML.set(key, newEditEl.innerHTML);
420
- }
421
- const recent = editHistory.slice(-5).map(e => ` - Line ${e.line}: "${truncate(e.oldText, 50)}" -> "${truncate(e.newText, 50)}"`);
422
- callbacks.updateContext(`User edited ${filePath} via preview editor (${editHistory.length} edit${editHistory.length !== 1 ? 's' : ''}):\n${recent.join('\n')}`);
423
- }
424
- catch (err) {
425
- // Revert on failure
426
- if (origHTML !== undefined)
427
- editEl.innerHTML = origHTML;
428
- callbacks.trackEvent?.('md_edit_failed', {
429
- block_type: block.type,
430
- error: String(err),
431
- });
432
- }
433
- }
434
- // --- Track which block the user is in via focusin ---
435
- function handleFocusIn(e) {
436
- const target = e.target;
437
- const blockEl = target.closest('.md-block--editable');
438
- if (!blockEl)
439
- return;
440
- if (blockEl !== activeBlockEl) {
441
- // Switching blocks — commit the previous one
442
- if (activeBlockEl && activeEditEl) {
443
- void commitBlock(activeBlockEl, activeEditEl);
444
- }
445
- activeBlockEl = blockEl;
446
- activeEditEl = getEditableChild(blockEl);
447
- }
448
- }
449
- // --- Commit when focus leaves the editor entirely ---
450
- function handleFocusOut(e) {
451
- const related = e.relatedTarget;
452
- // Focus stayed inside the editor or toolbar — don't commit
453
- if (related && (container.contains(related) || toolbar.element.contains(related)))
454
- return;
455
- toolbar.hide();
456
- if (activeBlockEl && activeEditEl) {
457
- void commitBlock(activeBlockEl, activeEditEl);
458
- }
459
- activeBlockEl = null;
460
- activeEditEl = null;
461
- }
462
- // --- Keyboard shortcuts ---
463
- function handleKeydown(e) {
464
- if (!activeBlockEl || !activeEditEl)
465
- return;
466
- if (handleFormatKeydown(e))
467
- return;
468
- // Enter in headings → commit
469
- if (e.key === 'Enter' && activeBlockEl.dataset.mdType === 'heading') {
470
- e.preventDefault();
471
- toolbar.hide();
472
- void commitBlock(activeBlockEl, activeEditEl);
473
- activeBlockEl = null;
474
- activeEditEl = null;
475
- return;
476
- }
477
- // Escape → revert this block
478
- if (e.key === 'Escape') {
479
- e.preventDefault();
480
- toolbar.hide();
481
- const key = blockKey(activeBlockEl);
482
- const origHTML = originalHTML.get(key);
483
- if (origHTML !== undefined && activeEditEl) {
484
- activeEditEl.innerHTML = origHTML;
485
- }
486
- activeEditEl?.blur();
487
- activeBlockEl = null;
488
- activeEditEl = null;
489
- }
490
- }
491
- // --- Toolbar: show on any text selection in any editable block ---
492
- function handleSelectionChange() {
493
- const sel = document.getSelection();
494
- if (!sel || !sel.rangeCount)
495
- return;
496
- if (sel.isCollapsed) {
497
- // Keep toolbar visible if a dropdown is open (user is picking
498
- // a color/size), otherwise hide it.
499
- if (toolbar.isVisible && !toolbar.hasOpenDropdown) {
500
- toolbar.hide();
501
- }
502
- return;
503
- }
504
- // Check if the selection is within any editable block in this editor
505
- const anchor = sel.anchorNode;
506
- const focus = sel.focusNode;
507
- if (!anchor || !focus)
508
- return;
509
- if (!container.contains(anchor) || !container.contains(focus))
510
- return;
511
- const editEl = anchor.parentElement?.closest('[contenteditable="true"]');
512
- if (!editEl || !container.contains(editEl))
513
- return;
514
- const range = sel.getRangeAt(0);
515
- const selRect = range.getBoundingClientRect();
516
- const containerRect = container.getBoundingClientRect();
517
- toolbar.show(selRect, containerRect);
518
- }
519
- container.addEventListener('focusin', handleFocusIn);
520
- container.addEventListener('focusout', handleFocusOut);
521
- container.addEventListener('keydown', handleKeydown);
522
- document.addEventListener('selectionchange', handleSelectionChange);
523
- return {
524
- cleanup() {
525
- container.removeEventListener('focusin', handleFocusIn);
526
- container.removeEventListener('focusout', handleFocusOut);
527
- container.removeEventListener('keydown', handleKeydown);
528
- document.removeEventListener('selectionchange', handleSelectionChange);
529
- toolbar.destroy();
530
- },
531
- isEditing() {
532
- return activeEditEl !== null;
533
- },
534
- };
535
- }
@@ -1,25 +0,0 @@
1
- export interface Block {
2
- start: number;
3
- end: number;
4
- text: string;
5
- }
6
- /**
7
- * Split markdown source into top-level blocks with source ranges. The ranges
8
- * drive byte-exact preservation of untouched content when merging Milkdown's
9
- * re-serialized output back into the original on-disk source.
10
- */
11
- export declare function extractBlocks(source: string): Block[];
12
- export interface MergeReport {
13
- merged: string;
14
- preservedCount: number;
15
- rewrittenCount: number;
16
- /** Populated when block counts don't align and we fell back to the full output. */
17
- fallback?: string;
18
- }
19
- /**
20
- * Merge Milkdown's current serialization back into the original on-disk source
21
- * block-by-block. Blocks whose Milkdown representation is unchanged since mount
22
- * are emitted byte-identical to disk; edited blocks take Milkdown's output.
23
- * Separators between blocks come from the original source.
24
- */
25
- export declare function mergeBlocks(originalSource: string, originalBlocks: Block[], baselineBlocks: Block[], currentSerialization: string, currentBlocks: Block[]): MergeReport;
@@ -1,86 +0,0 @@
1
- // markdown-it ships without TypeScript typings in this setup; reuse the
2
- // existing ambient-import pattern from parser.ts.
3
- // @ts-expect-error markdown-it does not provide local TypeScript typings in this setup.
4
- import MarkdownIt from 'markdown-it';
5
- const parser = new MarkdownIt({ html: true });
6
- /**
7
- * Split markdown source into top-level blocks with source ranges. The ranges
8
- * drive byte-exact preservation of untouched content when merging Milkdown's
9
- * re-serialized output back into the original on-disk source.
10
- */
11
- export function extractBlocks(source) {
12
- const tokens = parser.parse(source, {});
13
- const lines = source.split('\n');
14
- const lineOffsets = [0];
15
- for (let i = 0; i < lines.length; i++) {
16
- lineOffsets.push(lineOffsets[i] + lines[i].length + 1);
17
- }
18
- const blocks = [];
19
- let lastEndLine = 0;
20
- for (const token of tokens) {
21
- if (!token.map || token.map.length < 2)
22
- continue;
23
- const [startLine, endLine] = token.map;
24
- const t = token.type;
25
- const isOpening = typeof t === 'string' && (t.endsWith('_open')
26
- || t === 'hr'
27
- || t === 'fence'
28
- || t === 'code_block'
29
- || t === 'html_block');
30
- if (!isOpening || startLine < lastEndLine)
31
- continue;
32
- const start = lineOffsets[startLine];
33
- const rawEnd = lineOffsets[endLine] ?? source.length;
34
- const end = Math.min(source.length, Math.max(start, rawEnd - 1));
35
- blocks.push({ start, end, text: source.slice(start, end) });
36
- lastEndLine = endLine;
37
- }
38
- return blocks;
39
- }
40
- /**
41
- * Merge Milkdown's current serialization back into the original on-disk source
42
- * block-by-block. Blocks whose Milkdown representation is unchanged since mount
43
- * are emitted byte-identical to disk; edited blocks take Milkdown's output.
44
- * Separators between blocks come from the original source.
45
- */
46
- export function mergeBlocks(originalSource, originalBlocks, baselineBlocks, currentSerialization, currentBlocks) {
47
- if (originalBlocks.length !== baselineBlocks.length) {
48
- return {
49
- merged: currentSerialization,
50
- preservedCount: 0,
51
- rewrittenCount: currentBlocks.length,
52
- fallback: `original parses to ${originalBlocks.length} blocks but baseline parses to ${baselineBlocks.length}`,
53
- };
54
- }
55
- if (baselineBlocks.length !== currentBlocks.length) {
56
- return {
57
- merged: currentSerialization,
58
- preservedCount: 0,
59
- rewrittenCount: currentBlocks.length,
60
- fallback: `block count changed (baseline=${baselineBlocks.length}, current=${currentBlocks.length})`,
61
- };
62
- }
63
- const parts = [];
64
- let preserved = 0;
65
- let rewritten = 0;
66
- parts.push(originalSource.slice(0, originalBlocks[0]?.start ?? 0));
67
- for (let i = 0; i < originalBlocks.length; i++) {
68
- if (baselineBlocks[i].text === currentBlocks[i].text) {
69
- parts.push(originalBlocks[i].text);
70
- preserved++;
71
- }
72
- else {
73
- parts.push(currentBlocks[i].text);
74
- rewritten++;
75
- }
76
- const nextStart = i + 1 < originalBlocks.length
77
- ? originalBlocks[i + 1].start
78
- : originalSource.length;
79
- parts.push(originalSource.slice(originalBlocks[i].end, nextStart));
80
- }
81
- return {
82
- merged: parts.join(''),
83
- preservedCount: preserved,
84
- rewrittenCount: rewritten,
85
- };
86
- }
@@ -1,13 +0,0 @@
1
- import type { MarkdownLinkHeading, MarkdownLinkSearchItem } from './editor.js';
2
- export declare function renderLinkModalHtml(): string;
3
- export interface LinkModalHandle {
4
- open(selectedText?: string): void;
5
- close(): void;
6
- destroy(): void;
7
- }
8
- export declare function attachLinkModal(options: {
9
- root: HTMLElement;
10
- searchLinks?: (query: string) => Promise<MarkdownLinkSearchItem[]>;
11
- loadHeadings?: (filePath: string) => Promise<MarkdownLinkHeading[]>;
12
- onInsert: (text: string) => void;
13
- }): LinkModalHandle;