ep_data_tables 0.0.6 → 0.0.8

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.
@@ -17,12 +17,13 @@ const ATTR_TABLE_JSON = 'tbljson';
17
17
  const ATTR_CELL = 'td';
18
18
  const ATTR_CLASS_PREFIX = 'tbljson-'; // For finding the class in DOM
19
19
  const log = (...m) => console.debug('[ep_data_tables:client_hooks]', ...m);
20
- log('version 0.0.6');
21
20
  const DELIMITER = '\u241F'; // Internal column delimiter (␟)
22
21
  // Use the same rare character inside the hidden span so acePostWriteDomLineHTML can
23
22
  // still find delimiters when it splits node.innerHTML.
24
23
  // Users never see this because the span is contenteditable=false and styled away.
25
24
  const HIDDEN_DELIM = DELIMITER;
25
+ // InputEvent inputTypes used by mobile autocorrect/IME commit
26
+ const INPUTTYPE_REPLACEMENT_TYPES = new Set(['insertReplacementText', 'insertFromComposition']);
26
27
 
27
28
  // helper for stable random ids
28
29
  const rand = () => Math.random().toString(36).slice(2, 8);
@@ -67,14 +68,19 @@ let isAndroidChromeComposition = false;
67
68
  let handledCurrentComposition = false;
68
69
  // Suppress all beforeinput insertText events during an Android Chrome IME composition
69
70
  let suppressBeforeInputInsertTextDuringComposition = false;
70
- // Helper to detect Android Chromium-family browsers (exclude iOS and Firefox)
71
- function isAndroidChromiumUA() {
71
+ // Helper to detect any Android browser (exclude iOS/Safari)
72
+ function isAndroidUA() {
72
73
  const ua = (navigator.userAgent || '').toLowerCase();
73
74
  const isAndroid = ua.includes('android');
74
75
  const isIOS = ua.includes('iphone') || ua.includes('ipad') || ua.includes('ipod') || ua.includes('crios');
75
- const isFirefox = ua.includes('firefox');
76
- const isChromiumFamily = ua.includes('chrome') || ua.includes('edg') || ua.includes('opr') || ua.includes('samsungbrowser') || ua.includes('vivaldi') || ua.includes('brave');
77
- return isAndroid && !isIOS && !isFirefox && isChromiumFamily;
76
+ // Safari on Android is rare (WebKit ports), but our exclusion target is iOS Safari; we exclude all iOS above
77
+ return isAndroid && !isIOS;
78
+ }
79
+
80
+ // Helper to detect any iOS browser (Safari, Chrome iOS, Firefox iOS, Edge iOS)
81
+ function isIOSUA() {
82
+ const ua = (navigator.userAgent || '').toLowerCase();
83
+ return ua.includes('iphone') || ua.includes('ipad') || ua.includes('ipod') || ua.includes('ios') || ua.includes('crios') || ua.includes('fxios') || ua.includes('edgios');
78
84
  }
79
85
 
80
86
  // ─────────────────── Reusable Helper Functions ───────────────────
@@ -122,16 +128,16 @@ function getTableLineMetadata(lineNum, editorInfo, docManager) {
122
128
  try {
123
129
  const metadata = JSON.parse(attribs);
124
130
  if (metadata && metadata.tblId) {
125
- // log(`${funcName}: Found metadata via attribute for line ${lineNum}`);
131
+ // log(`${funcName}: Found metadata via attribute for line ${lineNum}`);
126
132
  return metadata;
127
133
  }
128
134
  } catch (e) {
129
- // log(`${funcName}: Invalid JSON in tbljson attribute on line ${lineNum}:`, e.message);
135
+ // log(`${funcName}: Invalid JSON in tbljson attribute on line ${lineNum}:`, e.message);
130
136
  }
131
137
  }
132
138
 
133
139
  // Fallback for block-styled lines.
134
- // log(`${funcName}: No valid attribute on line ${lineNum}, checking DOM.`);
140
+ // log(`${funcName}: No valid attribute on line ${lineNum}, checking DOM.`);
135
141
  const rep = editorInfo.ace_getRep();
136
142
 
137
143
  // This is the fix: Get the lineNode directly from the rep. It's more reliable
@@ -140,7 +146,7 @@ function getTableLineMetadata(lineNum, editorInfo, docManager) {
140
146
  const lineNode = lineEntry?.lineNode;
141
147
 
142
148
  if (!lineNode) {
143
- // log(`${funcName}: Could not find line node in rep for line ${lineNum}`);
149
+ // log(`${funcName}: Could not find line node in rep for line ${lineNum}`);
144
150
  return null;
145
151
  }
146
152
 
@@ -152,7 +158,7 @@ function getTableLineMetadata(lineNum, editorInfo, docManager) {
152
158
  try {
153
159
  const decodedString = atob(encodedData);
154
160
  const metadata = JSON.parse(decodedString);
155
- // log(`${funcName}: Reconstructed metadata from DOM for line ${lineNum}:`, metadata);
161
+ // log(`${funcName}: Reconstructed metadata from DOM for line ${lineNum}:`, metadata);
156
162
  return metadata;
157
163
  } catch (e) {
158
164
  console.error(`${funcName}: Failed to decode/parse tbljson class on line ${lineNum}:`, e);
@@ -162,7 +168,7 @@ function getTableLineMetadata(lineNum, editorInfo, docManager) {
162
168
  }
163
169
  }
164
170
 
165
- // log(`${funcName}: Could not find table metadata for line ${lineNum} in DOM.`);
171
+ // log(`${funcName}: Could not find table metadata for line ${lineNum} in DOM.`);
166
172
  return null;
167
173
  } catch (e) {
168
174
  console.error(`[ep_data_tables] ${funcName}: Error getting metadata for line ${lineNum}:`, e);
@@ -183,7 +189,7 @@ function getTableLineMetadata(lineNum, editorInfo, docManager) {
183
189
  */
184
190
  function navigateToNextCell(currentLineNum, currentCellIndex, tableMetadata, shiftKey, editorInfo, docManager) {
185
191
  const funcName = 'navigateToNextCell';
186
- // log(`${funcName}: START - Current: Line=${currentLineNum}, Cell=${currentCellIndex}, Shift=${shiftKey}`);
192
+ // log(`${funcName}: START - Current: Line=${currentLineNum}, Cell=${currentCellIndex}, Shift=${shiftKey}`);
187
193
 
188
194
  try {
189
195
  let targetRow = tableMetadata.row;
@@ -207,12 +213,12 @@ function navigateToNextCell(currentLineNum, currentCellIndex, tableMetadata, shi
207
213
  }
208
214
  }
209
215
 
210
- // log(`${funcName}: Target coordinates - Row=${targetRow}, Col=${targetCol}`);
216
+ // log(`${funcName}: Target coordinates - Row=${targetRow}, Col=${targetCol}`);
211
217
 
212
218
  // Find the line number for the target row
213
219
  const targetLineNum = findLineForTableRow(tableMetadata.tblId, targetRow, editorInfo, docManager);
214
220
  if (targetLineNum === -1) {
215
- // log(`${funcName}: Could not find line for target row ${targetRow}`);
221
+ // log(`${funcName}: Could not find line for target row ${targetRow}`);
216
222
  return false;
217
223
  }
218
224
 
@@ -236,25 +242,25 @@ function navigateToNextCell(currentLineNum, currentCellIndex, tableMetadata, shi
236
242
  */
237
243
  function navigateToCellBelow(currentLineNum, currentCellIndex, tableMetadata, editorInfo, docManager) {
238
244
  const funcName = 'navigateToCellBelow';
239
- // log(`${funcName}: START - Current: Line=${currentLineNum}, Cell=${currentCellIndex}`);
245
+ // log(`${funcName}: START - Current: Line=${currentLineNum}, Cell=${currentCellIndex}`);
240
246
 
241
247
  try {
242
248
  const targetRow = tableMetadata.row + 1;
243
249
  const targetCol = currentCellIndex;
244
250
 
245
- // log(`${funcName}: Target coordinates - Row=${targetRow}, Col=${targetCol}`);
251
+ // log(`${funcName}: Target coordinates - Row=${targetRow}, Col=${targetCol}`);
246
252
 
247
253
  // Find the line number for the target row
248
254
  const targetLineNum = findLineForTableRow(tableMetadata.tblId, targetRow, editorInfo, docManager);
249
255
 
250
256
  if (targetLineNum !== -1) {
251
257
  // Found the row below, navigate to it.
252
- // log(`${funcName}: Found line for target row ${targetRow}, navigating.`);
258
+ // log(`${funcName}: Found line for target row ${targetRow}, navigating.`);
253
259
  return navigateToCell(targetLineNum, targetCol, editorInfo, docManager);
254
260
  } else {
255
261
  // Could not find the row below, we must be on the last line.
256
262
  // Create a new, empty line after the table.
257
- // log(`${funcName}: Could not find next row. Creating new line after table.`);
263
+ // log(`${funcName}: Could not find next row. Creating new line after table.`);
258
264
  const rep = editorInfo.ace_getRep();
259
265
  const lineTextLength = rep.lines.atIndex(currentLineNum).text.length;
260
266
  const endOfLinePos = [currentLineNum, lineTextLength];
@@ -272,7 +278,7 @@ function navigateToCellBelow(currentLineNum, currentCellIndex, tableMetadata, ed
272
278
  // We've now exited the table, so clear the last-clicked state.
273
279
  const editor = editorInfo.editor;
274
280
  if (editor) editor.ep_data_tables_last_clicked = null;
275
- // log(`${funcName}: Cleared last click info as we have exited the table.`);
281
+ // log(`${funcName}: Cleared last click info as we have exited the table.`);
276
282
 
277
283
  return true; // We handled it.
278
284
  }
@@ -292,12 +298,12 @@ function navigateToCellBelow(currentLineNum, currentCellIndex, tableMetadata, ed
292
298
  */
293
299
  function findLineForTableRow(tblId, targetRow, editorInfo, docManager) {
294
300
  const funcName = 'findLineForTableRow';
295
- // log(`${funcName}: Searching for tblId=${tblId}, row=${targetRow}`);
301
+ // log(`${funcName}: Searching for tblId=${tblId}, row=${targetRow}`);
296
302
 
297
303
  try {
298
304
  const rep = editorInfo.ace_getRep();
299
305
  if (!rep || !rep.lines) {
300
- // log(`${funcName}: Could not get rep or rep.lines`);
306
+ // log(`${funcName}: Could not get rep or rep.lines`);
301
307
  return -1;
302
308
  }
303
309
 
@@ -315,7 +321,7 @@ function findLineForTableRow(tblId, targetRow, editorInfo, docManager) {
315
321
  const domTblId = tableInDOM.getAttribute('data-tblId');
316
322
  const domRow = tableInDOM.getAttribute('data-row');
317
323
  if (domTblId === tblId && domRow !== null && parseInt(domRow, 10) === targetRow) {
318
- // log(`${funcName}: Found target via DOM: line ${lineIndex}`);
324
+ // log(`${funcName}: Found target via DOM: line ${lineIndex}`);
319
325
  return lineIndex;
320
326
  }
321
327
  }
@@ -325,7 +331,7 @@ function findLineForTableRow(tblId, targetRow, editorInfo, docManager) {
325
331
  if (lineAttrString) {
326
332
  const lineMetadata = JSON.parse(lineAttrString);
327
333
  if (lineMetadata.tblId === tblId && lineMetadata.row === targetRow) {
328
- // log(`${funcName}: Found target via attribute: line ${lineIndex}`);
334
+ // log(`${funcName}: Found target via attribute: line ${lineIndex}`);
329
335
  return lineIndex;
330
336
  }
331
337
  }
@@ -334,7 +340,7 @@ function findLineForTableRow(tblId, targetRow, editorInfo, docManager) {
334
340
  }
335
341
  }
336
342
 
337
- // log(`${funcName}: Target row not found`);
343
+ // log(`${funcName}: Target row not found`);
338
344
  return -1;
339
345
  } catch (e) {
340
346
  console.error(`[ep_data_tables] ${funcName}: Error searching for line:`, e);
@@ -352,19 +358,19 @@ function findLineForTableRow(tblId, targetRow, editorInfo, docManager) {
352
358
  */
353
359
  function navigateToCell(targetLineNum, targetCellIndex, editorInfo, docManager) {
354
360
  const funcName = 'navigateToCell';
355
- // log(`${funcName}: START - Target: Line=${targetLineNum}, Cell=${targetCellIndex}`);
361
+ // log(`${funcName}: START - Target: Line=${targetLineNum}, Cell=${targetCellIndex}`);
356
362
  let targetPos;
357
363
 
358
364
  try {
359
365
  const rep = editorInfo.ace_getRep();
360
366
  if (!rep || !rep.lines) {
361
- // log(`${funcName}: Could not get rep or rep.lines`);
367
+ // log(`${funcName}: Could not get rep or rep.lines`);
362
368
  return false;
363
369
  }
364
370
 
365
371
  const lineEntry = rep.lines.atIndex(targetLineNum);
366
372
  if (!lineEntry) {
367
- // log(`${funcName}: Could not get line entry for line ${targetLineNum}`);
373
+ // log(`${funcName}: Could not get line entry for line ${targetLineNum}`);
368
374
  return false;
369
375
  }
370
376
 
@@ -372,7 +378,7 @@ function navigateToCell(targetLineNum, targetCellIndex, editorInfo, docManager)
372
378
  const cells = lineText.split(DELIMITER);
373
379
 
374
380
  if (targetCellIndex >= cells.length) {
375
- // log(`${funcName}: Target cell ${targetCellIndex} doesn't exist (only ${cells.length} cells)`);
381
+ // log(`${funcName}: Target cell ${targetCellIndex} doesn't exist (only ${cells.length} cells)`);
376
382
  return false;
377
383
  }
378
384
 
@@ -399,12 +405,12 @@ function navigateToCell(targetLineNum, targetCellIndex, editorInfo, docManager)
399
405
  cellIndex: targetCellIndex,
400
406
  relativePos: targetCellContent.length,
401
407
  };
402
- // log(`${funcName}: Pre-emptively updated stored click info:`, editor.ep_data_tables_last_clicked);
408
+ // log(`${funcName}: Pre-emptively updated stored click info:`, editor.ep_data_tables_last_clicked);
403
409
  } else {
404
- // log(`${funcName}: Could not get table metadata for target line ${targetLineNum}, cannot update click info.`);
410
+ // log(`${funcName}: Could not get table metadata for target line ${targetLineNum}, cannot update click info.`);
405
411
  }
406
412
  } catch (e) {
407
- // log(`${funcName}: Could not update stored click info before navigation:`, e.message);
413
+ // log(`${funcName}: Could not update stored click info before navigation:`, e.message);
408
414
  }
409
415
 
410
416
  // The previous attempts involving wrappers and poking the renderer have all
@@ -414,17 +420,17 @@ function navigateToCell(targetLineNum, targetCellIndex, editorInfo, docManager)
414
420
  try {
415
421
  // 1. Update the internal representation of the selection.
416
422
  editorInfo.ace_performSelectionChange(targetPos, targetPos, false);
417
- // log(`${funcName}: Updated internal selection to [${targetPos}]`);
423
+ // log(`${funcName}: Updated internal selection to [${targetPos}]`);
418
424
 
419
425
  // 2. Explicitly tell the editor to update the browser's visual selection
420
426
  // to match the new internal representation. This is the correct way to
421
427
  // make the caret appear in the new location without causing a race condition.
422
428
  editorInfo.ace_updateBrowserSelectionFromRep();
423
- // log(`${funcName}: Called updateBrowserSelectionFromRep to sync visual caret.`);
429
+ // log(`${funcName}: Called updateBrowserSelectionFromRep to sync visual caret.`);
424
430
 
425
431
  // 3. Ensure the editor has focus.
426
432
  editorInfo.ace_focus();
427
- // log(`${funcName}: Editor focused.`);
433
+ // log(`${funcName}: Editor focused.`);
428
434
 
429
435
  } catch(e) {
430
436
  console.error(`[ep_data_tables] ${funcName}: Error during direct navigation update:`, e);
@@ -437,7 +443,7 @@ function navigateToCell(targetLineNum, targetCellIndex, editorInfo, docManager)
437
443
  return false;
438
444
  }
439
445
 
440
- // log(`${funcName}: Navigation considered successful.`);
446
+ // log(`${funcName}: Navigation considered successful.`);
441
447
  return true;
442
448
  }
443
449
 
@@ -448,36 +454,36 @@ exports.collectContentPre = (hook, ctx) => {
448
454
  const state = ctx.state;
449
455
  const cc = ctx.cc; // ContentCollector instance
450
456
 
451
- // log(`${funcName}: *** ENTRY POINT *** Hook: ${hook}, Node: ${node?.tagName}.${node?.className}`);
457
+ // log(`${funcName}: *** ENTRY POINT *** Hook: ${hook}, Node: ${node?.tagName}.${node?.className}`);
452
458
 
453
459
  // ***** START Primary Path: Reconstruct from rendered table *****
454
460
  if (node?.classList?.contains('ace-line')) {
455
461
  const tableNode = node.querySelector('table.dataTable[data-tblId]');
456
462
  if (tableNode) {
457
- // log(`${funcName}: Found ace-line with rendered table. Attempting reconstruction from DOM.`);
463
+ // log(`${funcName}: Found ace-line with rendered table. Attempting reconstruction from DOM.`);
458
464
 
459
465
  const docManager = cc.documentAttributeManager;
460
466
  const rep = cc.rep;
461
467
  const lineNum = rep?.lines?.indexOfKey(node.id);
462
468
 
463
469
  if (typeof lineNum === 'number' && lineNum >= 0 && docManager) {
464
- // log(`${funcName}: Processing line ${lineNum} (NodeID: ${node.id}) for DOM reconstruction.`);
470
+ // log(`${funcName}: Processing line ${lineNum} (NodeID: ${node.id}) for DOM reconstruction.`);
465
471
  try {
466
472
  const existingAttrString = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
467
- // log(`${funcName}: Line ${lineNum} existing ${ATTR_TABLE_JSON} attribute: '${existingAttrString}'`);
473
+ // log(`${funcName}: Line ${lineNum} existing ${ATTR_TABLE_JSON} attribute: '${existingAttrString}'`);
468
474
 
469
475
  if (existingAttrString) {
470
476
  const existingMetadata = JSON.parse(existingAttrString);
471
477
  if (existingMetadata && typeof existingMetadata.tblId !== 'undefined' &&
472
478
  typeof existingMetadata.row !== 'undefined' && typeof existingMetadata.cols === 'number') {
473
- // log(`${funcName}: Line ${lineNum} existing metadata is valid:`, existingMetadata);
479
+ // log(`${funcName}: Line ${lineNum} existing metadata is valid:`, existingMetadata);
474
480
 
475
481
  const trNode = tableNode.querySelector('tbody > tr');
476
482
  if (trNode) {
477
- // log(`${funcName}: Line ${lineNum} found <tr> node for cell content extraction.`);
483
+ // log(`${funcName}: Line ${lineNum} found <tr> node for cell content extraction.`);
478
484
  let cellHTMLSegments = Array.from(trNode.children).map((td, index) => {
479
485
  let segmentHTML = td.innerHTML || '';
480
- // log(`${funcName}: Line ${lineNum} TD[${index}] raw innerHTML (first 100): "${segmentHTML.substring(0,100)}"`);
486
+ // log(`${funcName}: Line ${lineNum} TD[${index}] raw innerHTML (first 100): "${segmentHTML.substring(0,100)}"`);
481
487
 
482
488
  const resizeHandleRegex = /<div class="ep-data_tables-resize-handle"[^>]*><\/div>/ig;
483
489
  segmentHTML = segmentHTML.replace(resizeHandleRegex, '');
@@ -498,44 +504,44 @@ exports.collectContentPre = (hook, ctx) => {
498
504
  const hidden = index === 0 ? '' :
499
505
  /* keep the char in the DOM but make it visually disappear and non-editable */
500
506
  `<span class="ep-data_tables-delim" contenteditable="false">${HIDDEN_DELIM}</span>`;
501
- // log(`${funcName}: Line ${lineNum} TD[${index}] cleaned innerHTML (first 100): "${segmentHTML.substring(0,100)}"`);
507
+ // log(`${funcName}: Line ${lineNum} TD[${index}] cleaned innerHTML (first 100): "${segmentHTML.substring(0,100)}"`);
502
508
  return segmentHTML;
503
509
  });
504
510
 
505
511
  if (cellHTMLSegments.length !== existingMetadata.cols) {
506
- // log(`${funcName}: WARNING Line ${lineNum}: Reconstructed cell count (${cellHTMLSegments.length}) does not match metadata cols (${existingMetadata.cols}). Padding/truncating.`);
512
+ // log(`${funcName}: WARNING Line ${lineNum}: Reconstructed cell count (${cellHTMLSegments.length}) does not match metadata cols (${existingMetadata.cols}). Padding/truncating.`);
507
513
  while (cellHTMLSegments.length < existingMetadata.cols) cellHTMLSegments.push('');
508
514
  if (cellHTMLSegments.length > existingMetadata.cols) cellHTMLSegments.length = existingMetadata.cols;
509
515
  }
510
516
 
511
517
  const canonicalLineText = cellHTMLSegments.join(DELIMITER);
512
518
  state.line = canonicalLineText;
513
- // log(`${funcName}: Line ${lineNum} successfully reconstructed ctx.state.line: "${canonicalLineText.substring(0, 200)}..."`);
519
+ // log(`${funcName}: Line ${lineNum} successfully reconstructed ctx.state.line: "${canonicalLineText.substring(0, 200)}..."`);
514
520
 
515
521
  state.lineAttributes = state.lineAttributes || [];
516
522
  state.lineAttributes = state.lineAttributes.filter(attr => attr[0] !== ATTR_TABLE_JSON);
517
523
  state.lineAttributes.push([ATTR_TABLE_JSON, existingAttrString]);
518
- // log(`${funcName}: Line ${lineNum} ensured ${ATTR_TABLE_JSON} attribute is in state.lineAttributes.`);
524
+ // log(`${funcName}: Line ${lineNum} ensured ${ATTR_TABLE_JSON} attribute is in state.lineAttributes.`);
519
525
 
520
- // log(`${funcName}: Line ${lineNum} reconstruction complete. Returning undefined to prevent default DOM collection.`);
526
+ // log(`${funcName}: Line ${lineNum} reconstruction complete. Returning undefined to prevent default DOM collection.`);
521
527
  return undefined;
522
528
  } else {
523
- // log(`${funcName}: ERROR Line ${lineNum}: Could not find tbody > tr in rendered table for reconstruction.`);
529
+ // log(`${funcName}: ERROR Line ${lineNum}: Could not find tbody > tr in rendered table for reconstruction.`);
524
530
  }
525
531
  } else {
526
- // log(`${funcName}: ERROR Line ${lineNum}: Invalid or incomplete existing metadata from line attribute:`, existingMetadata);
532
+ // log(`${funcName}: ERROR Line ${lineNum}: Invalid or incomplete existing metadata from line attribute:`, existingMetadata);
527
533
  }
528
534
  } else {
529
- // log(`${funcName}: WARNING Line ${lineNum}: No existing ${ATTR_TABLE_JSON} attribute found for reconstruction, despite table DOM presence. Table may be malformed or attribute lost.`);
535
+ // log(`${funcName}: WARNING Line ${lineNum}: No existing ${ATTR_TABLE_JSON} attribute found for reconstruction, despite table DOM presence. Table may be malformed or attribute lost.`);
530
536
  const domTblId = tableNode.getAttribute('data-tblId');
531
537
  const domRow = tableNode.getAttribute('data-row');
532
538
  const trNode = tableNode.querySelector('tbody > tr');
533
539
  if (domTblId && domRow !== null && trNode && trNode.children.length > 0) {
534
- // log(`${funcName}: Line ${lineNum} FALLBACK: Attempting reconstruction using table DOM attributes as ${ATTR_TABLE_JSON} was missing.`);
540
+ // log(`${funcName}: Line ${lineNum} FALLBACK: Attempting reconstruction using table DOM attributes as ${ATTR_TABLE_JSON} was missing.`);
535
541
  const domCols = trNode.children.length;
536
542
  const tempMetadata = {tblId: domTblId, row: parseInt(domRow, 10), cols: domCols};
537
543
  const tempAttrString = JSON.stringify(tempMetadata);
538
- // log(`${funcName}: Line ${lineNum} FALLBACK: Constructed temporary metadata: ${tempAttrString}`);
544
+ // log(`${funcName}: Line ${lineNum} FALLBACK: Constructed temporary metadata: ${tempAttrString}`);
539
545
 
540
546
  let cellHTMLSegments = Array.from(trNode.children).map((td, index) => {
541
547
  let segmentHTML = td.innerHTML || '';
@@ -555,7 +561,7 @@ exports.collectContentPre = (hook, ctx) => {
555
561
  });
556
562
 
557
563
  if (cellHTMLSegments.length !== domCols) {
558
- // log(`${funcName}: WARNING Line ${lineNum} (Fallback): Reconstructed cell count (${cellHTMLSegments.length}) does not match DOM cols (${domCols}).`);
564
+ // log(`${funcName}: WARNING Line ${lineNum} (Fallback): Reconstructed cell count (${cellHTMLSegments.length}) does not match DOM cols (${domCols}).`);
559
565
  while(cellHTMLSegments.length < domCols) cellHTMLSegments.push('');
560
566
  if(cellHTMLSegments.length > domCols) cellHTMLSegments.length = domCols;
561
567
  }
@@ -565,24 +571,24 @@ exports.collectContentPre = (hook, ctx) => {
565
571
  state.lineAttributes = state.lineAttributes || [];
566
572
  state.lineAttributes = state.lineAttributes.filter(attr => attr[0] !== ATTR_TABLE_JSON);
567
573
  state.lineAttributes.push([ATTR_TABLE_JSON, tempAttrString]);
568
- // log(`${funcName}: Line ${lineNum} FALLBACK: Successfully reconstructed line using DOM attributes. Returning undefined.`);
574
+ // log(`${funcName}: Line ${lineNum} FALLBACK: Successfully reconstructed line using DOM attributes. Returning undefined.`);
569
575
  return undefined;
570
576
  } else {
571
- // log(`${funcName}: Line ${lineNum} FALLBACK: Could not reconstruct from DOM attributes due to missing info.`);
577
+ // log(`${funcName}: Line ${lineNum} FALLBACK: Could not reconstruct from DOM attributes due to missing info.`);
572
578
  }
573
579
  }
574
580
  } catch (e) {
575
581
  console.error(`[ep_data_tables] ${funcName}: Line ${lineNum} error during DOM reconstruction:`, e);
576
- // log(`${funcName}: Line ${lineNum} Exception details:`, { message: e.message, stack: e.stack });
582
+ // log(`${funcName}: Line ${lineNum} Exception details:`, { message: e.message, stack: e.stack });
577
583
  }
578
584
  } else {
579
- // log(`${funcName}: Could not get valid line number (${lineNum}), rep, or docManager for DOM reconstruction of ace-line.`);
585
+ // log(`${funcName}: Could not get valid line number (${lineNum}), rep, or docManager for DOM reconstruction of ace-line.`);
580
586
  }
581
587
  } else {
582
- // log(`${funcName}: Node is ace-line but no rendered table.dataTable[data-tblId] found. Allowing normal processing for: ${node?.className}`);
588
+ // log(`${funcName}: Node is ace-line but no rendered table.dataTable[data-tblId] found. Allowing normal processing for: ${node?.className}`);
583
589
  }
584
590
  } else {
585
- // log(`${funcName}: Node is not an ace-line (or node is null). Node: ${node?.tagName}.${node?.className}. Allowing normal processing.`);
591
+ // log(`${funcName}: Node is not an ace-line (or node is null). Node: ${node?.tagName}.${node?.className}. Allowing normal processing.`);
586
592
  }
587
593
  // ***** END Primary Path *****
588
594
 
@@ -591,19 +597,19 @@ exports.collectContentPre = (hook, ctx) => {
591
597
  const classes = ctx.cls ? ctx.cls.split(' ') : [];
592
598
  let appliedAttribFromClass = false;
593
599
  if (classes.length > 0) {
594
- // log(`${funcName}: Secondary path - Checking classes on node ${node?.tagName}.${node?.className}: [${classes.join(', ')}]`);
600
+ // log(`${funcName}: Secondary path - Checking classes on node ${node?.tagName}.${node?.className}: [${classes.join(', ')}]`);
595
601
  for (const cls of classes) {
596
602
  if (cls.startsWith('tbljson-')) {
597
- // log(`${funcName}: Secondary path - Found tbljson class: ${cls} on node ${node?.tagName}.${node?.className}`);
603
+ // log(`${funcName}: Secondary path - Found tbljson class: ${cls} on node ${node?.tagName}.${node?.className}`);
598
604
  const encodedMetadata = cls.substring(8);
599
605
  try {
600
606
  const decodedMetadata = dec(encodedMetadata);
601
607
  if (decodedMetadata) {
602
608
  cc.doAttrib(state, `${ATTR_TABLE_JSON}::${decodedMetadata}`);
603
609
  appliedAttribFromClass = true;
604
- // log(`${funcName}: Secondary path - Applied attribute to OP via cc.doAttrib for class ${cls.substring(0, 20)}... on ${node?.tagName}`);
610
+ // log(`${funcName}: Secondary path - Applied attribute to OP via cc.doAttrib for class ${cls.substring(0, 20)}... on ${node?.tagName}`);
605
611
  } else {
606
- // log(`${funcName}: Secondary path - ERROR - Decoded metadata is null or empty for class ${cls}`);
612
+ // log(`${funcName}: Secondary path - ERROR - Decoded metadata is null or empty for class ${cls}`);
607
613
  }
608
614
  } catch (e) {
609
615
  console.error(`[ep_data_tables] ${funcName}: Secondary path - Error processing tbljson class ${cls} on ${node?.tagName}:`, e);
@@ -612,49 +618,49 @@ exports.collectContentPre = (hook, ctx) => {
612
618
  }
613
619
  }
614
620
  if (!appliedAttribFromClass && classes.some(c => c.startsWith('tbljson-'))) {
615
- // log(`${funcName}: Secondary path - Found tbljson- class but failed to apply attribute.`);
621
+ // log(`${funcName}: Secondary path - Found tbljson- class but failed to apply attribute.`);
616
622
  } else if (!classes.some(c => c.startsWith('tbljson-'))) {
617
- // log(`${funcName}: Secondary path - No tbljson- class found on this node.`);
623
+ // log(`${funcName}: Secondary path - No tbljson- class found on this node.`);
618
624
  }
619
625
  } else {
620
- // log(`${funcName}: Secondary path - Node ${node?.tagName}.${node?.className} has no ctx.cls or classes array is empty.`);
626
+ // log(`${funcName}: Secondary path - Node ${node?.tagName}.${node?.className} has no ctx.cls or classes array is empty.`);
621
627
  }
622
628
 
623
- // log(`${funcName}: *** EXIT POINT *** For Node: ${node?.tagName}.${node?.className}. Applied from class: ${appliedAttribFromClass}`);
629
+ // log(`${funcName}: *** EXIT POINT *** For Node: ${node?.tagName}.${node?.className}. Applied from class: ${appliedAttribFromClass}`);
624
630
  };
625
631
 
626
632
  // ───────────── attribute → span‑class mapping (linestylefilter hook) ─────────
627
633
  exports.aceAttribsToClasses = (hook, ctx) => {
628
634
  const funcName = 'aceAttribsToClasses';
629
- // log(`>>>> ${funcName}: Called with key: ${ctx.key}`); // log entry
635
+ // log(`>>>> ${funcName}: Called with key: ${ctx.key}`); // log entry
630
636
  if (ctx.key === ATTR_TABLE_JSON) {
631
- // log(`${funcName}: Processing ATTR_TABLE_JSON.`);
637
+ // log(`${funcName}: Processing ATTR_TABLE_JSON.`);
632
638
  // ctx.value is the raw JSON string from Etherpad's attribute pool
633
639
  const rawJsonValue = ctx.value;
634
- // log(`${funcName}: Received raw attribute value (ctx.value):`, rawJsonValue);
640
+ // log(`${funcName}: Received raw attribute value (ctx.value):`, rawJsonValue);
635
641
 
636
642
  // Attempt to parse for logging purposes
637
643
  let parsedMetadataForLog = '[JSON Parse Error]';
638
644
  try {
639
645
  parsedMetadataForLog = JSON.parse(rawJsonValue);
640
- // log(`${funcName}: Value parsed for logging:`, parsedMetadataForLog);
646
+ // log(`${funcName}: Value parsed for logging:`, parsedMetadataForLog);
641
647
  } catch(e) {
642
- // log(`${funcName}: Error parsing raw JSON value for logging:`, e);
648
+ // log(`${funcName}: Error parsing raw JSON value for logging:`, e);
643
649
  // Continue anyway, enc() might still work if it's just a string
644
650
  }
645
651
 
646
652
  // Generate the class name by base64 encoding the raw JSON string.
647
653
  // This ensures acePostWriteDomLineHTML receives the expected encoded format.
648
654
  const className = `tbljson-${enc(rawJsonValue)}`;
649
- // log(`${funcName}: Generated class name by encoding raw JSON: ${className}`);
655
+ // log(`${funcName}: Generated class name by encoding raw JSON: ${className}`);
650
656
  return [className];
651
657
  }
652
658
  if (ctx.key === ATTR_CELL) {
653
659
  // Keep this in case we want cell-specific styling later
654
- // // log(`${funcName}: Processing ATTR_CELL: ${ctx.value}`); // Optional: Uncomment if needed
660
+ //// log(`${funcName}: Processing ATTR_CELL: ${ctx.value}`); // Optional: Uncomment if needed
655
661
  return [`tblCell-${ctx.value}`];
656
662
  }
657
- // // log(`${funcName}: Processing other key: ${ctx.key}`); // Optional: Uncomment if needed
663
+ //// log(`${funcName}: Processing other key: ${ctx.key}`); // Optional: Uncomment if needed
658
664
  return [];
659
665
  };
660
666
 
@@ -673,15 +679,14 @@ function escapeHtml(text = '') {
673
679
  };
674
680
  return strText.replace(/[&<>"'']/g, function(m) { return map[m]; });
675
681
  }
676
-
677
682
  // NEW Helper function to build table HTML from pre-rendered delimited content with resize handles
678
683
  function buildTableFromDelimitedHTML(metadata, innerHTMLSegments) {
679
684
  const funcName = 'buildTableFromDelimitedHTML';
680
- // log(`${funcName}: START`, { metadata, innerHTMLSegments });
685
+ // log(`${funcName}: START`, { metadata, innerHTMLSegments });
681
686
 
682
687
  if (!metadata || typeof metadata.tblId === 'undefined' || typeof metadata.row === 'undefined') {
683
688
  console.error(`[ep_data_tables] ${funcName}: Invalid or missing metadata. Aborting.`);
684
- // log(`${funcName}: END - Error`);
689
+ // log(`${funcName}: END - Error`);
685
690
  return '<table class="dataTable dataTable-error"><tbody><tr><td>Error: Missing table metadata</td></tr></tbody></table>'; // Return error table
686
691
  }
687
692
 
@@ -736,6 +741,42 @@ function buildTableFromDelimitedHTML(metadata, innerHTMLSegments) {
736
741
  ? `<span class="${encodedTbljsonClass ? `${encodedTbljsonClass} ` : ''}tblCell-${index}">${modifiedSegment}${caretAnchorSpan}</span>`
737
742
  : `${modifiedSegment}${caretAnchorSpan}`);
738
743
 
744
+ // Normalize: ensure first content span has correct tblCell-N and strip tblCell-* from subsequent spans
745
+ try {
746
+ const requiredCellClass = `tblCell-${index}`;
747
+ // Skip a leading delimiter span if present
748
+ const leadingDelimMatch = modifiedSegment.match(/^\s*<span[^>]*\bep-data_tables-delim\b[^>]*>[\s\S]*?<\/span>\s*/i);
749
+ const head = leadingDelimMatch ? leadingDelimMatch[0] : '';
750
+ const tail = leadingDelimMatch ? modifiedSegment.slice(head.length) : modifiedSegment;
751
+
752
+ const openSpanMatch = tail.match(/^\s*<span([^>]*)>/i);
753
+ if (!openSpanMatch) {
754
+ // No span at start: wrap with required cell class. Keep tbljson if available for stability.
755
+ const baseClasses = `${encodedTbljsonClass ? `${encodedTbljsonClass} ` : ''}${requiredCellClass}`;
756
+ modifiedSegment = `${head}<span class="${baseClasses}">${tail}</span>`;
757
+ } else {
758
+ const fullOpen = openSpanMatch[0];
759
+ const attrs = openSpanMatch[1] || '';
760
+ const classMatch = /\bclass\s*=\s*"([^"]*)"/i.exec(attrs);
761
+ let classList = classMatch ? classMatch[1].split(/\s+/).filter(Boolean) : [];
762
+ // Remove any stale tblCell-* from the first span's class list
763
+ classList = classList.filter(c => !/^tblCell-\d+$/.test(c));
764
+ // Ensure required cell class exists
765
+ classList.push(requiredCellClass);
766
+ const unique = Array.from(new Set(classList));
767
+ const newClassAttr = ` class="${unique.join(' ')}"`;
768
+ const attrsWithoutClass = classMatch ? attrs.replace(/\s*class\s*=\s*"[^"]*"/i, '') : attrs;
769
+ const rebuiltOpen = `<span${newClassAttr}${attrsWithoutClass}>`;
770
+ const afterOpen = tail.slice(fullOpen.length);
771
+ // Remove tblCell-* from any subsequent spans to avoid cascading mismatches (keep tbljson- as-is)
772
+ const cleanedTail = afterOpen.replace(/(<span[^>]*class=")([^"]*)(")/ig, (m, p1, classes, p3) => {
773
+ const filtered = classes.split(/\s+/).filter(c => c && !/^tblCell-\d+$/.test(c)).join(' ');
774
+ return p1 + filtered + p3;
775
+ });
776
+ modifiedSegment = head + rebuiltOpen + cleanedTail;
777
+ }
778
+ } catch (_) { /* ignore normalization errors */ }
779
+
739
780
  // Width & other decorations remain unchanged
740
781
  const widthPercent = columnWidths[index] || (100 / numCols);
741
782
  const cellStyle = `${tdStyle} width: ${widthPercent}%;`;
@@ -747,17 +788,17 @@ function buildTableFromDelimitedHTML(metadata, innerHTMLSegments) {
747
788
  const tdContent = `<td style="${cellStyle}" data-column="${index}" draggable="false">${modifiedSegment}${resizeHandle}</td>`;
748
789
  return tdContent;
749
790
  }).join('');
750
- // log(`${funcName}: Joined all cellsHtml:`, cellsHtml);
791
+ // log(`${funcName}: Joined all cellsHtml:`, cellsHtml);
751
792
 
752
793
  // Add 'dataTable-first-row' class if it's the logical first row (row index 0)
753
794
  const firstRowClass = metadata.row === 0 ? ' dataTable-first-row' : '';
754
- // log(`${funcName}: First row class applied: '${firstRowClass}'`);
795
+ // log(`${funcName}: First row class applied: '${firstRowClass}'`);
755
796
 
756
797
  // Construct the final table HTML
757
798
  // Rely on CSS for border-collapse, width etc. Add data attributes from metadata.
758
- const tableHtml = `<table class="dataTable${firstRowClass}" data-tblId="${metadata.tblId}" data-row="${metadata.row}" style="width:100%; border-collapse: collapse; table-layout: fixed;" draggable="false"><tbody><tr>${cellsHtml}</tr></tbody></table>`;
759
- // log(`${funcName}: Generated final table HTML:`, tableHtml);
760
- // log(`${funcName}: END - Success`);
799
+ const tableHtml = `<table class="dataTable${firstRowClass}" writingsuggestions="false" data-tblId="${metadata.tblId}" data-row="${metadata.row}" style="width:100%; border-collapse: collapse; table-layout: fixed;" draggable="false"><tbody><tr>${cellsHtml}</tr></tbody></table>`;
800
+ // log(`${funcName}: Generated final table HTML:`, tableHtml);
801
+ // log(`${funcName}: END - Success`);
761
802
  return tableHtml;
762
803
  }
763
804
 
@@ -770,28 +811,28 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
770
811
  const logPrefix = '[ep_data_tables:acePostWriteDomLineHTML]'; // Consistent prefix
771
812
 
772
813
  // *** STARTUP LOGGING ***
773
- // log(`${logPrefix} ----- START ----- NodeID: ${nodeId} LineNum: ${lineNum}`);
814
+ // log(`${logPrefix} ----- START ----- NodeID: ${nodeId} LineNum: ${lineNum}`);
774
815
  if (!node || !nodeId) {
775
- // log(`${logPrefix} ERROR - Received invalid node or node without ID. Aborting.`);
816
+ // log(`${logPrefix} ERROR - Received invalid node or node without ID. Aborting.`);
776
817
  console.error(`[ep_data_tables] ${funcName}: Received invalid node or node without ID.`);
777
818
  return cb();
778
819
  }
779
820
 
780
821
  // *** ENHANCED DEBUG: Log complete DOM state ***
781
- // log(`${logPrefix} NodeID#${nodeId}: COMPLETE DOM STRUCTURE DEBUG:`);
782
- // log(`${logPrefix} NodeID#${nodeId}: Node tagName: ${node.tagName}`);
783
- // log(`${logPrefix} NodeID#${nodeId}: Node className: ${node.className}`);
784
- // log(`${logPrefix} NodeID#${nodeId}: Node innerHTML length: ${node.innerHTML?.length || 0}`);
785
- // log(`${logPrefix} NodeID#${nodeId}: Node innerHTML (first 500 chars): "${(node.innerHTML || '').substring(0, 500)}"`);
786
- // log(`${logPrefix} NodeID#${nodeId}: Node children count: ${node.children?.length || 0}`);
822
+ // log(`${logPrefix} NodeID#${nodeId}: COMPLETE DOM STRUCTURE DEBUG:`);
823
+ // log(`${logPrefix} NodeID#${nodeId}: Node tagName: ${node.tagName}`);
824
+ // log(`${logPrefix} NodeID#${nodeId}: Node className: ${node.className}`);
825
+ // log(`${logPrefix} NodeID#${nodeId}: Node innerHTML length: ${node.innerHTML?.length || 0}`);
826
+ // log(`${logPrefix} NodeID#${nodeId}: Node innerHTML (first 500 chars): "${(node.innerHTML || '').substring(0, 500)}"`);
827
+ // log(`${logPrefix} NodeID#${nodeId}: Node children count: ${node.children?.length || 0}`);
787
828
 
788
829
  // log all child elements and their classes
789
830
  if (node.children) {
790
831
  for (let i = 0; i < Math.min(node.children.length, 10); i++) {
791
832
  const child = node.children[i];
792
- // log(`${logPrefix} NodeID#${nodeId}: Child[${i}] tagName: ${child.tagName}, className: "${child.className}", innerHTML length: ${child.innerHTML?.length || 0}`);
833
+ // log(`${logPrefix} NodeID#${nodeId}: Child[${i}] tagName: ${child.tagName}, className: "${child.className}", innerHTML length: ${child.innerHTML?.length || 0}`);
793
834
  if (child.className && child.className.includes('tbljson-')) {
794
- // log(`${logPrefix} NodeID#${nodeId}: *** FOUND TBLJSON CLASS ON CHILD[${i}] ***`);
835
+ // log(`${logPrefix} NodeID#${nodeId}: *** FOUND TBLJSON CLASS ON CHILD[${i}] ***`);
795
836
  }
796
837
  }
797
838
  }
@@ -800,69 +841,69 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
800
841
  let encodedJsonString = null;
801
842
 
802
843
  // --- 1. Find and Parse Metadata Attribute ---
803
- // log(`${logPrefix} NodeID#${nodeId}: Searching for tbljson-* class...`);
844
+ // log(`${logPrefix} NodeID#${nodeId}: Searching for tbljson-* class...`);
804
845
 
805
846
  // ENHANCED Helper function to recursively search for tbljson class in all descendants
806
847
  function findTbljsonClass(element, depth = 0, path = '') {
807
848
  const indent = ' '.repeat(depth);
808
- // log(`${logPrefix} NodeID#${nodeId}: ${indent}Searching element: ${element.tagName || 'unknown'}, path: ${path}`);
849
+ // log(`${logPrefix} NodeID#${nodeId}: ${indent}Searching element: ${element.tagName || 'unknown'}, path: ${path}`);
809
850
 
810
851
  // Check the element itself
811
852
  if (element.classList) {
812
- // log(`${logPrefix} NodeID#${nodeId}: ${indent}Element has ${element.classList.length} classes: [${Array.from(element.classList).join(', ')}]`);
853
+ // log(`${logPrefix} NodeID#${nodeId}: ${indent}Element has ${element.classList.length} classes: [${Array.from(element.classList).join(', ')}]`);
813
854
  for (const cls of element.classList) {
814
855
  if (cls.startsWith('tbljson-')) {
815
- // log(`${logPrefix} NodeID#${nodeId}: ${indent}*** FOUND TBLJSON CLASS: ${cls.substring(8)} at depth ${depth}, path: ${path} ***`);
856
+ // log(`${logPrefix} NodeID#${nodeId}: ${indent}*** FOUND TBLJSON CLASS: ${cls.substring(8)} at depth ${depth}, path: ${path} ***`);
816
857
  return cls.substring(8);
817
858
  }
818
859
  }
819
860
  } else {
820
- // log(`${logPrefix} NodeID#${nodeId}: ${indent}Element has no classList`);
861
+ // log(`${logPrefix} NodeID#${nodeId}: ${indent}Element has no classList`);
821
862
  }
822
863
 
823
864
  // Recursively check all descendants
824
865
  if (element.children) {
825
- // log(`${logPrefix} NodeID#${nodeId}: ${indent}Element has ${element.children.length} children`);
866
+ // log(`${logPrefix} NodeID#${nodeId}: ${indent}Element has ${element.children.length} children`);
826
867
  for (let i = 0; i < element.children.length; i++) {
827
868
  const child = element.children[i];
828
869
  const childPath = `${path}>${child.tagName}[${i}]`;
829
870
  const found = findTbljsonClass(child, depth + 1, childPath);
830
871
  if (found) {
831
- // log(`${logPrefix} NodeID#${nodeId}: ${indent}Returning found result from child: ${found}`);
872
+ // log(`${logPrefix} NodeID#${nodeId}: ${indent}Returning found result from child: ${found}`);
832
873
  return found;
833
874
  }
834
875
  }
835
876
  } else {
836
- // log(`${logPrefix} NodeID#${nodeId}: ${indent}Element has no children`);
877
+ // log(`${logPrefix} NodeID#${nodeId}: ${indent}Element has no children`);
837
878
  }
838
879
 
839
- // log(`${logPrefix} NodeID#${nodeId}: ${indent}No tbljson class found in this element or its children`);
880
+ // log(`${logPrefix} NodeID#${nodeId}: ${indent}No tbljson class found in this element or its children`);
840
881
  return null;
841
882
  }
842
883
 
843
884
  // Search for tbljson class starting from the node
844
- // log(`${logPrefix} NodeID#${nodeId}: Starting recursive search for tbljson class...`);
885
+ // log(`${logPrefix} NodeID#${nodeId}: Starting recursive search for tbljson class...`);
845
886
  encodedJsonString = findTbljsonClass(node, 0, 'ROOT');
846
887
 
847
888
  if (encodedJsonString) {
848
- // log(`${logPrefix} NodeID#${nodeId}: *** SUCCESS: Found encoded tbljson class: ${encodedJsonString} ***`);
889
+ // log(`${logPrefix} NodeID#${nodeId}: *** SUCCESS: Found encoded tbljson class: ${encodedJsonString} ***`);
849
890
  } else {
850
- // log(`${logPrefix} NodeID#${nodeId}: *** NO TBLJSON CLASS FOUND ***`);
891
+ // log(`${logPrefix} NodeID#${nodeId}: *** NO TBLJSON CLASS FOUND ***`);
851
892
  }
852
893
 
853
894
  // If no attribute found, it's not a table line managed by us
854
895
  if (!encodedJsonString) {
855
- // log(`${logPrefix} NodeID#${nodeId}: No tbljson-* class found. Assuming not a table line. END.`);
896
+ // log(`${logPrefix} NodeID#${nodeId}: No tbljson-* class found. Assuming not a table line. END.`);
856
897
 
857
898
  // DEBUG: Add detailed logging to understand why tbljson class is missing
858
- // log(`${logPrefix} NodeID#${nodeId}: DEBUG - Node tag: ${node.tagName}, Node classes:`, Array.from(node.classList || []));
859
- // log(`${logPrefix} NodeID#${nodeId}: DEBUG - Node innerHTML (first 200 chars): "${(node.innerHTML || '').substring(0, 200)}"`);
899
+ // log(`${logPrefix} NodeID#${nodeId}: DEBUG - Node tag: ${node.tagName}, Node classes:`, Array.from(node.classList || []));
900
+ // log(`${logPrefix} NodeID#${nodeId}: DEBUG - Node innerHTML (first 200 chars): "${(node.innerHTML || '').substring(0, 200)}"`);
860
901
 
861
902
  // Check if there are any child elements with classes
862
903
  if (node.children && node.children.length > 0) {
863
904
  for (let i = 0; i < Math.min(node.children.length, 5); i++) {
864
905
  const child = node.children[i];
865
- // log(`${logPrefix} NodeID#${nodeId}: DEBUG - Child ${i} tag: ${child.tagName}, classes:`, Array.from(child.classList || []));
906
+ // log(`${logPrefix} NodeID#${nodeId}: DEBUG - Child ${i} tag: ${child.tagName}, classes:`, Array.from(child.classList || []));
866
907
  }
867
908
  }
868
909
 
@@ -871,24 +912,24 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
871
912
  if (existingTable) {
872
913
  const existingTblId = existingTable.getAttribute('data-tblId');
873
914
  const existingRow = existingTable.getAttribute('data-row');
874
- // log(`${logPrefix} NodeID#${nodeId}: DEBUG - Found orphaned table! TblId: ${existingTblId}, Row: ${existingRow}`);
915
+ // log(`${logPrefix} NodeID#${nodeId}: DEBUG - Found orphaned table! TblId: ${existingTblId}, Row: ${existingRow}`);
875
916
 
876
917
  // This suggests the table exists but the tbljson class was lost
877
918
  // Check if we're in a post-resize situation
878
919
  if (existingTblId && existingRow !== null) {
879
- // log(`${logPrefix} NodeID#${nodeId}: POTENTIAL ISSUE - Table exists but no tbljson class. This may be a post-resize issue.`);
920
+ // log(`${logPrefix} NodeID#${nodeId}: POTENTIAL ISSUE - Table exists but no tbljson class. This may be a post-resize issue.`);
880
921
 
881
922
  // Try to look up what the metadata should be based on the table attributes
882
923
  const tableCells = existingTable.querySelectorAll('td');
883
- // log(`${logPrefix} NodeID#${nodeId}: Table has ${tableCells.length} cells`);
924
+ // log(`${logPrefix} NodeID#${nodeId}: Table has ${tableCells.length} cells`);
884
925
 
885
926
  // log the current line's attribute state if we can get line number
886
927
  if (lineNum !== undefined && args?.documentAttributeManager) {
887
928
  try {
888
929
  const currentLineAttr = args.documentAttributeManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
889
- // log(`${logPrefix} NodeID#${nodeId}: Current line ${lineNum} tbljson attribute: ${currentLineAttr || 'NULL'}`);
930
+ // log(`${logPrefix} NodeID#${nodeId}: Current line ${lineNum} tbljson attribute: ${currentLineAttr || 'NULL'}`);
890
931
  } catch (e) {
891
- // log(`${logPrefix} NodeID#${nodeId}: Error getting line attribute:`, e);
932
+ // log(`${logPrefix} NodeID#${nodeId}: Error getting line attribute:`, e);
892
933
  }
893
934
  }
894
935
  }
@@ -900,7 +941,7 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
900
941
  // *** NEW CHECK: If table already rendered, skip regeneration ***
901
942
  const existingTable = node.querySelector('table.dataTable[data-tblId]');
902
943
  if (existingTable) {
903
- // log(`${logPrefix} NodeID#${nodeId}: Table already exists in DOM. Skipping innerHTML replacement.`);
944
+ // log(`${logPrefix} NodeID#${nodeId}: Table already exists in DOM. Skipping innerHTML replacement.`);
904
945
  // Optionally, verify tblId matches metadata? For now, assume it's correct.
905
946
  // const existingTblId = existingTable.getAttribute('data-tblId');
906
947
  // try {
@@ -911,26 +952,26 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
911
952
  return cb(); // Do nothing further
912
953
  }
913
954
 
914
- // log(`${logPrefix} NodeID#${nodeId}: Decoding and parsing metadata...`);
955
+ // log(`${logPrefix} NodeID#${nodeId}: Decoding and parsing metadata...`);
915
956
  try {
916
957
  const decoded = dec(encodedJsonString);
917
- // log(`${logPrefix} NodeID#${nodeId}: Decoded string: ${decoded}`);
958
+ // log(`${logPrefix} NodeID#${nodeId}: Decoded string: ${decoded}`);
918
959
  if (!decoded) throw new Error('Decoded string is null or empty.');
919
960
  rowMetadata = JSON.parse(decoded);
920
- // log(`${logPrefix} NodeID#${nodeId}: Parsed rowMetadata:`, rowMetadata);
961
+ // log(`${logPrefix} NodeID#${nodeId}: Parsed rowMetadata:`, rowMetadata);
921
962
 
922
963
  // Validate essential metadata
923
964
  if (!rowMetadata || typeof rowMetadata.tblId === 'undefined' || typeof rowMetadata.row === 'undefined' || typeof rowMetadata.cols !== 'number') {
924
965
  throw new Error('Invalid or incomplete metadata (missing tblId, row, or cols).');
925
966
  }
926
- // log(`${logPrefix} NodeID#${nodeId}: Metadata validated successfully.`);
967
+ // log(`${logPrefix} NodeID#${nodeId}: Metadata validated successfully.`);
927
968
 
928
969
  } catch(e) {
929
- // log(`${logPrefix} NodeID#${nodeId}: FATAL ERROR - Failed to decode/parse/validate tbljson metadata. Rendering cannot proceed.`, e);
970
+ // log(`${logPrefix} NodeID#${nodeId}: FATAL ERROR - Failed to decode/parse/validate tbljson metadata. Rendering cannot proceed.`, e);
930
971
  console.error(`[ep_data_tables] ${funcName} NodeID#${nodeId}: Failed to decode/parse/validate tbljson.`, encodedJsonString, e);
931
972
  // Optionally render an error state in the node?
932
973
  node.innerHTML = '<div style="color:red; border: 1px solid red; padding: 5px;">[ep_data_tables] Error: Invalid table metadata attribute found.</div>';
933
- // log(`${logPrefix} NodeID#${nodeId}: Rendered error message in node. END.`);
974
+ // log(`${logPrefix} NodeID#${nodeId}: Rendered error message in node. END.`);
934
975
  return cb();
935
976
  }
936
977
  // --- End Metadata Parsing ---
@@ -944,131 +985,86 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
944
985
  // After an edit, aceKeyEvent updates atext, and node.innerHTML reflects that new "EditedCell1|Cell2" string.
945
986
  // When styling is applied, it will include spans like: <span class="author-xxx bold">Cell1</span>|<span class="author-yyy italic">Cell2</span>
946
987
  const delimitedTextFromLine = node.innerHTML;
947
- // log(`${logPrefix} NodeID#${nodeId}: Using node.innerHTML for delimited text to preserve styling.`);
948
- // log(`${logPrefix} NodeID#${nodeId}: Raw innerHTML length: ${delimitedTextFromLine?.length || 0}`);
949
- // log(`${logPrefix} NodeID#${nodeId}: Raw innerHTML (first 1000 chars): "${(delimitedTextFromLine || '').substring(0, 1000)}"`);
988
+ // log(`${logPrefix} NodeID#${nodeId}: Using node.innerHTML for delimited text to preserve styling.`);
989
+ // log(`${logPrefix} NodeID#${nodeId}: Raw innerHTML length: ${delimitedTextFromLine?.length || 0}`);
990
+ // log(`${logPrefix} NodeID#${nodeId}: Raw innerHTML (first 1000 chars): "${(delimitedTextFromLine || '').substring(0, 1000)}"`);
950
991
 
951
992
  // *** ENHANCED DEBUG: Analyze delimiter presence ***
952
993
  const delimiterCount = (delimitedTextFromLine || '').split(DELIMITER).length - 1;
953
- // log(`${logPrefix} NodeID#${nodeId}: Delimiter '${DELIMITER}' count in innerHTML: ${delimiterCount}`);
954
- // log(`${logPrefix} NodeID#${nodeId}: Expected delimiters for ${rowMetadata.cols} columns: ${rowMetadata.cols - 1}`);
994
+ // log(`${logPrefix} NodeID#${nodeId}: Delimiter '${DELIMITER}' count in innerHTML: ${delimiterCount}`);
995
+ // log(`${logPrefix} NodeID#${nodeId}: Expected delimiters for ${rowMetadata.cols} columns: ${rowMetadata.cols - 1}`);
955
996
 
956
997
  // log all delimiter positions
957
998
  let pos = -1;
958
999
  const delimiterPositions = [];
959
1000
  while ((pos = delimitedTextFromLine.indexOf(DELIMITER, pos + 1)) !== -1) {
960
1001
  delimiterPositions.push(pos);
961
- // log(`${logPrefix} NodeID#${nodeId}: Delimiter found at position ${pos}, context: "${delimitedTextFromLine.substring(Math.max(0, pos - 20), pos + 21)}"`);
1002
+ // log(`${logPrefix} NodeID#${nodeId}: Delimiter found at position ${pos}, context: "${delimitedTextFromLine.substring(Math.max(0, pos - 20), pos + 21)}"`);
962
1003
  }
963
- // log(`${logPrefix} NodeID#${nodeId}: All delimiter positions: [${delimiterPositions.join(', ')}]`);
1004
+ // log(`${logPrefix} NodeID#${nodeId}: All delimiter positions: [${delimiterPositions.join(', ')}]`);
964
1005
 
965
1006
  // The DELIMITER const is defined at the top of this file.
966
1007
  // NEW: Remove all hidden-delimiter <span> wrappers **before** we split so
967
1008
  // the embedded delimiter character they carry doesn't inflate or shrink
968
1009
  // the segment count.
969
- const spanDelimRegex = new RegExp('<span class="ep-data_tables-delim"[^>]*>' + DELIMITER + '</span>', 'ig');
970
- // Safari-specific normalization: it may serialize the delimiter as entities and inject Apple spans
971
- const delimiterEntityHexRE = /&#x241f;/ig; // hex entity for U+241F
972
- const delimiterEntityDecRE = /&#9247;/g; // decimal entity for U+241F
973
- const appleConvertedSpaceRE = /<span class="Apple-converted-space">[\s\u00A0]*<\/span>/ig;
974
- const zeroWidthCharsRE = /[\u200B\u200C\u200D\uFEFF]/g;
975
-
976
- const hexMatches = ((delimitedTextFromLine || '').match(delimiterEntityHexRE) || []).length;
977
- const decMatches = ((delimitedTextFromLine || '').match(delimiterEntityDecRE) || []).length;
978
- const appleSpaceMatches = ((delimitedTextFromLine || '').match(appleConvertedSpaceRE) || []).length;
979
-
1010
+ // Preserve the delimiter character when stripping its wrapper span so splitting works
1011
+ const spanDelimRegex = /<span class="ep-data_tables-delim"[^>]*>[\s\S]*?<\/span>/ig;
980
1012
  const sanitizedHTMLForSplit = (delimitedTextFromLine || '')
981
- .replace(spanDelimRegex, '')
1013
+ .replace(spanDelimRegex, DELIMITER)
982
1014
  // strip caret anchors from raw line html before split
983
1015
  .replace(/<span class="ep-data_tables-caret-anchor"[^>]*><\/span>/ig, '')
984
- // Safari may serialize the delimiter as HTML entities – convert back to raw char
985
- .replace(delimiterEntityHexRE, DELIMITER)
986
- .replace(delimiterEntityDecRE, DELIMITER)
987
- // Safari sometimes injects Apple-converted-space wrappers; collapse to a normal space
988
- .replace(appleConvertedSpaceRE, ' ')
989
- // Guard against invisible characters that can disturb splitting
990
- .replace(zeroWidthCharsRE, '');
991
-
992
- if ((hexMatches + decMatches + appleSpaceMatches) > 0) {
993
- console.warn(`[ep_data_tables] ${funcName} NodeID#${nodeId}: Normalized Safari entities/spans before splitting: hex=${hexMatches}, dec=${decMatches}, appleSpaces=${appleSpaceMatches}`);
994
- }
995
-
1016
+ .replace(/\r?\n/g, ' ')
1017
+ .replace(/\u00A0/gu, ' ')
1018
+ .replace(/<br\s*\/?>/gi, ' ');
996
1019
  const htmlSegments = sanitizedHTMLForSplit.split(DELIMITER);
997
1020
 
998
- // log(`${logPrefix} NodeID#${nodeId}: *** SEGMENT ANALYSIS ***`);
999
- // log(`${logPrefix} NodeID#${nodeId}: Split resulted in ${htmlSegments.length} segments`);
1021
+ // log(`${logPrefix} NodeID#${nodeId}: *** SEGMENT ANALYSIS ***`);
1022
+ // log(`${logPrefix} NodeID#${nodeId}: Split resulted in ${htmlSegments.length} segments`);
1000
1023
  for (let i = 0; i < htmlSegments.length; i++) {
1001
1024
  const segment = htmlSegments[i] || '';
1002
- // log(`${logPrefix} NodeID#${nodeId}: Segment[${i}] length: ${segment.length}`);
1003
- // log(`${logPrefix} NodeID#${nodeId}: Segment[${i}] content (first 200 chars): "${segment.substring(0, 200)}"`);
1025
+ // log(`${logPrefix} NodeID#${nodeId}: Segment[${i}] length: ${segment.length}`);
1026
+ // log(`${logPrefix} NodeID#${nodeId}: Segment[${i}] content (first 200 chars): "${segment.substring(0, 200)}"`);
1004
1027
  if (segment.length > 200) {
1005
- // log(`${logPrefix} NodeID#${nodeId}: Segment[${i}] content (chars 200-400): "${segment.substring(200, 400)}"`);
1028
+ // log(`${logPrefix} NodeID#${nodeId}: Segment[${i}] content (chars 200-400): "${segment.substring(200, 400)}"`);
1006
1029
  }
1007
1030
  if (segment.length > 400) {
1008
- // log(`${logPrefix} NodeID#${nodeId}: Segment[${i}] content (chars 400-600): "${segment.substring(400, 600)}"`);
1031
+ // log(`${logPrefix} NodeID#${nodeId}: Segment[${i}] content (chars 400-600): "${segment.substring(400, 600)}"`);
1009
1032
  }
1010
1033
  // Check if segment contains image-related content
1011
1034
  if (segment.includes('image:') || segment.includes('image-placeholder') || segment.includes('currently-selected')) {
1012
- // log(`${logPrefix} NodeID#${nodeId}: *** SEGMENT[${i}] CONTAINS IMAGE CONTENT ***`);
1035
+ // log(`${logPrefix} NodeID#${nodeId}: *** SEGMENT[${i}] CONTAINS IMAGE CONTENT ***`);
1013
1036
  }
1037
+ // Diagnostics: surface suspicious class patterns that can precede breakage
1038
+ try {
1039
+ const tblCellMatches = segment.match(/\btblCell-(\d+)\b/g) || [];
1040
+ const tbljsonMatches = segment.match(/\btbljson-[A-Za-z0-9_-]+\b/g) || [];
1041
+ const uniqueCells = Array.from(new Set(tblCellMatches));
1042
+ if (uniqueCells.length > 1) {
1043
+ console.warn('[ep_data_tables][diag] segment contains multiple tblCell-* markers', { segIndex: i, uniqueCells });
1044
+ }
1045
+ // intentionally ignore duplicate tbljson-* occurrences – not root cause
1046
+ } catch (_) {}
1014
1047
  }
1015
1048
 
1016
- // log(`${logPrefix} NodeID#${nodeId}: Parsed HTML segments (${htmlSegments.length}):`, htmlSegments.map(s => (s || '').substring(0,50) + (s && s.length > 50 ? '...' : '')));
1049
+ // log(`${logPrefix} NodeID#${nodeId}: Parsed HTML segments (${htmlSegments.length}):`, htmlSegments.map(s => (s || '').substring(0,50) + (s && s.length > 50 ? '...' : '')));
1017
1050
 
1018
1051
  // --- Enhanced Validation with Automatic Structure Reconstruction ---
1019
1052
  let finalHtmlSegments = htmlSegments;
1020
1053
 
1021
1054
  if (htmlSegments.length !== rowMetadata.cols) {
1022
- // log(`${logPrefix} NodeID#${nodeId}: *** MISMATCH DETECTED *** - Attempting reconstruction.`);
1023
-
1055
+ // log(`${logPrefix} NodeID#${nodeId}: *** MISMATCH DETECTED *** - Attempting reconstruction.`);
1056
+ console.warn('[ep_data_tables][diag] Segment/column mismatch', { nodeId, lineNum, segs: htmlSegments.length, cols: rowMetadata.cols, tblId: rowMetadata.tblId, row: rowMetadata.row });
1057
+
1024
1058
  // Check if this is an image selection issue
1025
1059
  const hasImageSelected = delimitedTextFromLine.includes('currently-selected');
1026
1060
  const hasImageContent = delimitedTextFromLine.includes('image:');
1027
1061
  if (hasImageSelected) {
1028
- // log(`${logPrefix} NodeID#${nodeId}: *** POTENTIAL CAUSE: Image selection state may be affecting segment parsing ***`);
1062
+ // log(`${logPrefix} NodeID#${nodeId}: *** POTENTIAL CAUSE: Image selection state may be affecting segment parsing ***`);
1029
1063
  }
1030
1064
 
1031
- // First attempt: reconstruct using DOM spans that carry tblCell-N classes
1065
+ // First attempt (DISABLED): class-based reconstruction caused content loss in real pads.
1066
+ // We now rely on sanitized delimiter splitting and safe padding/truncation instead.
1032
1067
  let usedClassReconstruction = false;
1033
- try {
1034
- const cols = Math.max(0, Number(rowMetadata.cols) || 0);
1035
- const grouped = Array.from({ length: cols }, () => '');
1036
- const candidates = Array.from(node.querySelectorAll('[class*="tblCell-"]'));
1037
-
1038
- const classNum = (el) => {
1039
- if (!el || !el.classList) return -1;
1040
- for (const cls of el.classList) {
1041
- const m = /^tblCell-(\d+)$/.exec(cls);
1042
- if (m) return parseInt(m[1], 10);
1043
- }
1044
- return -1;
1045
- };
1046
- const hasAncestorWithSameCell = (el, n) => {
1047
- let p = el?.parentElement;
1048
- while (p) {
1049
- if (p.classList && p.classList.contains(`tblCell-${n}`)) return true;
1050
- p = p.parentElement;
1051
- }
1052
- return false;
1053
- };
1054
-
1055
- for (const el of candidates) {
1056
- const n = classNum(el);
1057
- if (n >= 0 && n < cols) {
1058
- if (!hasAncestorWithSameCell(el, n)) {
1059
- grouped[n] += el.outerHTML || '';
1060
- }
1061
- }
1062
- }
1063
- const usable = grouped.some(s => s && s.trim() !== '');
1064
- if (usable) {
1065
- finalHtmlSegments = grouped.map(s => (s && s.trim() !== '') ? s : '&nbsp;');
1066
- usedClassReconstruction = true;
1067
- console.warn(`[ep_data_tables] ${funcName} NodeID#${nodeId}: Reconstructed ${finalHtmlSegments.length} segments from tblCell-N classes.`);
1068
- }
1069
- } catch (e) {
1070
- console.debug(`[ep_data_tables] ${funcName} NodeID#${nodeId}: Class-based reconstruction error; falling back.`, e);
1071
- }
1072
1068
 
1073
1069
  // Fallback: reconstruct from string segments
1074
1070
  if (!usedClassReconstruction) {
@@ -1097,14 +1093,14 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
1097
1093
  console.warn(`[ep_data_tables] ${funcName} NodeID#${nodeId}: Could not reconstruct to expected ${rowMetadata.cols} segments. Got ${finalHtmlSegments.length}.`);
1098
1094
  }
1099
1095
  } else {
1100
- // log(`${logPrefix} NodeID#${nodeId}: Segment count matches metadata cols (${rowMetadata.cols}). Using original segments.`);
1096
+ // log(`${logPrefix} NodeID#${nodeId}: Segment count matches metadata cols (${rowMetadata.cols}). Using original segments.`);
1101
1097
  }
1102
1098
 
1103
1099
  // --- 3. Build and Render Table ---
1104
- // log(`${logPrefix} NodeID#${nodeId}: Calling buildTableFromDelimitedHTML...`);
1100
+ // log(`${logPrefix} NodeID#${nodeId}: Calling buildTableFromDelimitedHTML...`);
1105
1101
  try {
1106
1102
  const newTableHTML = buildTableFromDelimitedHTML(rowMetadata, finalHtmlSegments);
1107
- // log(`${logPrefix} NodeID#${nodeId}: Received new table HTML from helper. Replacing content.`);
1103
+ // log(`${logPrefix} NodeID#${nodeId}: Received new table HTML from helper. Replacing content.`);
1108
1104
 
1109
1105
  // The old local findTbljsonElement is removed from here. We use the global one now.
1110
1106
  const tbljsonElement = findTbljsonElement(node);
@@ -1117,24 +1113,24 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
1117
1113
  const blockElements = ['center', 'div', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'right', 'left', 'ul', 'ol', 'li', 'code'];
1118
1114
 
1119
1115
  if (blockElements.includes(parentTag)) {
1120
- // log(`${logPrefix} NodeID#${nodeId}: Preserving block element ${parentTag} and replacing its content with table.`);
1116
+ // log(`${logPrefix} NodeID#${nodeId}: Preserving block element ${parentTag} and replacing its content with table.`);
1121
1117
  tbljsonElement.parentElement.innerHTML = newTableHTML;
1122
1118
  } else {
1123
- // log(`${logPrefix} NodeID#${nodeId}: Parent element ${parentTag} is not a block element, replacing entire node content.`);
1119
+ // log(`${logPrefix} NodeID#${nodeId}: Parent element ${parentTag} is not a block element, replacing entire node content.`);
1124
1120
  node.innerHTML = newTableHTML;
1125
1121
  }
1126
1122
  } else {
1127
1123
  // Replace the node's content entirely with the generated table
1128
- // log(`${logPrefix} NodeID#${nodeId}: No nested block element found, replacing entire node content.`);
1124
+ // log(`${logPrefix} NodeID#${nodeId}: No nested block element found, replacing entire node content.`);
1129
1125
  node.innerHTML = newTableHTML;
1130
1126
  }
1131
1127
 
1132
- // log(`${logPrefix} NodeID#${nodeId}: Successfully replaced content with new table structure.`);
1128
+ // log(`${logPrefix} NodeID#${nodeId}: Successfully replaced content with new table structure.`);
1133
1129
  } catch (renderError) {
1134
- // log(`${logPrefix} NodeID#${nodeId}: ERROR during table building or rendering.`, renderError);
1130
+ // log(`${logPrefix} NodeID#${nodeId}: ERROR during table building or rendering.`, renderError);
1135
1131
  console.error(`[ep_data_tables] ${funcName} NodeID#${nodeId}: Error building/rendering table.`, renderError);
1136
1132
  node.innerHTML = '<div style="color:red; border: 1px solid red; padding: 5px;">[ep_data_tables] Error: Failed to render table structure.</div>';
1137
- // log(`${logPrefix} NodeID#${nodeId}: Rendered build/render error message in node. END.`);
1133
+ // log(`${logPrefix} NodeID#${nodeId}: Rendered build/render error message in node. END.`);
1138
1134
  return cb();
1139
1135
  }
1140
1136
  // --- End Table Building ---
@@ -1142,7 +1138,7 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
1142
1138
  // *** REMOVED CACHING LOGIC ***
1143
1139
  // The old logic based on tableRowNodes cache is completely removed.
1144
1140
 
1145
- // log(`${logPrefix}: ----- END ----- NodeID: ${nodeId}`);
1141
+ // log(`${logPrefix}: ----- END ----- NodeID: ${nodeId}`);
1146
1142
  return cb();
1147
1143
  };
1148
1144
 
@@ -1157,7 +1153,6 @@ function _getLineNumberOfElement(element) {
1157
1153
  }
1158
1154
  return count;
1159
1155
  }
1160
-
1161
1156
  // ───────────────────── Handle Key Events ─────────────────────
1162
1157
  exports.aceKeyEvent = (h, ctx) => {
1163
1158
  const funcName = 'aceKeyEvent';
@@ -1168,34 +1163,34 @@ exports.aceKeyEvent = (h, ctx) => {
1168
1163
 
1169
1164
  const startLogTime = Date.now();
1170
1165
  const logPrefix = '[ep_data_tables:aceKeyEvent]';
1171
- // log(`${logPrefix} START Key='${evt?.key}' Code=${evt?.keyCode} Type=${evt?.type} Modifiers={ctrl:${evt?.ctrlKey},alt:${evt?.altKey},meta:${evt?.metaKey},shift:${evt?.shiftKey}}`, { selStart: rep?.selStart, selEnd: rep?.selEnd });
1166
+ // log(`${logPrefix} START Key='${evt?.key}' Code=${evt?.keyCode} Type=${evt?.type} Modifiers={ctrl:${evt?.ctrlKey},alt:${evt?.altKey},meta:${evt?.metaKey},shift:${evt?.shiftKey}}`, { selStart: rep?.selStart, selEnd: rep?.selEnd });
1172
1167
 
1173
1168
  if (!rep || !rep.selStart || !editorInfo || !evt || !docManager) {
1174
- // log(`${logPrefix} Skipping - Missing critical context.`);
1169
+ // log(`${logPrefix} Skipping - Missing critical context.`);
1175
1170
  return false;
1176
1171
  }
1177
1172
 
1178
1173
  // Get caret info from event context - may be stale
1179
1174
  const reportedLineNum = rep.selStart[0];
1180
1175
  const reportedCol = rep.selStart[1];
1181
- // log(`${logPrefix} Reported caret from rep: Line=${reportedLineNum}, Col=${reportedCol}`);
1176
+ // log(`${logPrefix} Reported caret from rep: Line=${reportedLineNum}, Col=${reportedCol}`);
1182
1177
 
1183
1178
  // --- Get Table Metadata for the reported line ---
1184
1179
  let tableMetadata = null;
1185
1180
  let lineAttrString = null; // Store for potential use later
1186
1181
  try {
1187
1182
  // Add debugging to see what's happening with attribute retrieval
1188
- // log(`${logPrefix} DEBUG: Attempting to get ${ATTR_TABLE_JSON} attribute from line ${reportedLineNum}`);
1183
+ // log(`${logPrefix} DEBUG: Attempting to get ${ATTR_TABLE_JSON} attribute from line ${reportedLineNum}`);
1189
1184
  lineAttrString = docManager.getAttributeOnLine(reportedLineNum, ATTR_TABLE_JSON);
1190
- // log(`${logPrefix} DEBUG: getAttributeOnLine returned: ${lineAttrString ? `"${lineAttrString}"` : 'null/undefined'}`);
1185
+ // log(`${logPrefix} DEBUG: getAttributeOnLine returned: ${lineAttrString ? `"${lineAttrString}"` : 'null/undefined'}`);
1191
1186
 
1192
1187
  // Also check if there are any attributes on this line at all
1193
1188
  if (typeof docManager.getAttributesOnLine === 'function') {
1194
1189
  try {
1195
1190
  const allAttribs = docManager.getAttributesOnLine(reportedLineNum);
1196
- // log(`${logPrefix} DEBUG: All attributes on line ${reportedLineNum}:`, allAttribs);
1191
+ // log(`${logPrefix} DEBUG: All attributes on line ${reportedLineNum}:`, allAttribs);
1197
1192
  } catch(e) {
1198
- // log(`${logPrefix} DEBUG: Error getting all attributes:`, e);
1193
+ // log(`${logPrefix} DEBUG: Error getting all attributes:`, e);
1199
1194
  }
1200
1195
  }
1201
1196
 
@@ -1209,34 +1204,34 @@ exports.aceKeyEvent = (h, ctx) => {
1209
1204
  if (tableInDOM) {
1210
1205
  const domTblId = tableInDOM.getAttribute('data-tblId');
1211
1206
  const domRow = tableInDOM.getAttribute('data-row');
1212
- // log(`${logPrefix} DEBUG: Found table in DOM without attribute! TblId=${domTblId}, Row=${domRow}`);
1207
+ // log(`${logPrefix} DEBUG: Found table in DOM without attribute! TblId=${domTblId}, Row=${domRow}`);
1213
1208
  // Try to reconstruct the metadata from DOM
1214
1209
  const domCells = tableInDOM.querySelectorAll('td');
1215
1210
  if (domTblId && domRow !== null && domCells.length > 0) {
1216
- // log(`${logPrefix} DEBUG: Attempting to reconstruct metadata from DOM...`);
1211
+ // log(`${logPrefix} DEBUG: Attempting to reconstruct metadata from DOM...`);
1217
1212
  const reconstructedMetadata = {
1218
1213
  tblId: domTblId,
1219
1214
  row: parseInt(domRow, 10),
1220
1215
  cols: domCells.length
1221
1216
  };
1222
1217
  lineAttrString = JSON.stringify(reconstructedMetadata);
1223
- // log(`${logPrefix} DEBUG: Reconstructed metadata: ${lineAttrString}`);
1218
+ // log(`${logPrefix} DEBUG: Reconstructed metadata: ${lineAttrString}`);
1224
1219
  }
1225
1220
  }
1226
1221
  }
1227
1222
  } catch(e) {
1228
- // log(`${logPrefix} DEBUG: Error checking DOM for table:`, e);
1223
+ // log(`${logPrefix} DEBUG: Error checking DOM for table:`, e);
1229
1224
  }
1230
1225
  }
1231
1226
 
1232
1227
  if (lineAttrString) {
1233
1228
  tableMetadata = JSON.parse(lineAttrString);
1234
1229
  if (!tableMetadata || typeof tableMetadata.cols !== 'number') {
1235
- // log(`${logPrefix} Line ${reportedLineNum} has attribute, but metadata invalid/missing cols.`);
1230
+ // log(`${logPrefix} Line ${reportedLineNum} has attribute, but metadata invalid/missing cols.`);
1236
1231
  tableMetadata = null; // Ensure it's null if invalid
1237
1232
  }
1238
1233
  } else {
1239
- // log(`${logPrefix} DEBUG: No ${ATTR_TABLE_JSON} attribute found on line ${reportedLineNum}`);
1234
+ // log(`${logPrefix} DEBUG: No ${ATTR_TABLE_JSON} attribute found on line ${reportedLineNum}`);
1240
1235
  // Not a table line based on reported caret line
1241
1236
  }
1242
1237
  } catch(e) {
@@ -1247,7 +1242,7 @@ exports.aceKeyEvent = (h, ctx) => {
1247
1242
  // Get last known good state
1248
1243
  const editor = editorInfo.editor; // Get editor instance
1249
1244
  const lastClick = editor?.ep_data_tables_last_clicked; // Read shared state
1250
- // log(`${logPrefix} Reading stored click/caret info:`, lastClick);
1245
+ // log(`${logPrefix} Reading stored click/caret info:`, lastClick);
1251
1246
 
1252
1247
  // --- Determine the TRUE target line, cell, and caret position ---
1253
1248
  let currentLineNum = -1;
@@ -1262,22 +1257,22 @@ exports.aceKeyEvent = (h, ctx) => {
1262
1257
 
1263
1258
  // ** Scenario 1: Try to trust lastClick info **
1264
1259
  if (lastClick) {
1265
- // log(`${logPrefix} Attempting to validate stored click info for Line=${lastClick.lineNum}...`);
1260
+ // log(`${logPrefix} Attempting to validate stored click info for Line=${lastClick.lineNum}...`);
1266
1261
  let storedLineAttrString = null;
1267
1262
  let storedLineMetadata = null;
1268
1263
  try {
1269
- // log(`${logPrefix} DEBUG: Getting ${ATTR_TABLE_JSON} attribute from stored line ${lastClick.lineNum}`);
1264
+ // log(`${logPrefix} DEBUG: Getting ${ATTR_TABLE_JSON} attribute from stored line ${lastClick.lineNum}`);
1270
1265
  storedLineAttrString = docManager.getAttributeOnLine(lastClick.lineNum, ATTR_TABLE_JSON);
1271
- // log(`${logPrefix} DEBUG: Stored line attribute result: ${storedLineAttrString ? `"${storedLineAttrString}"` : 'null/undefined'}`);
1266
+ // log(`${logPrefix} DEBUG: Stored line attribute result: ${storedLineAttrString ? `"${storedLineAttrString}"` : 'null/undefined'}`);
1272
1267
 
1273
1268
  if (storedLineAttrString) {
1274
1269
  storedLineMetadata = JSON.parse(storedLineAttrString);
1275
- // log(`${logPrefix} DEBUG: Parsed stored metadata:`, storedLineMetadata);
1270
+ // log(`${logPrefix} DEBUG: Parsed stored metadata:`, storedLineMetadata);
1276
1271
  }
1277
1272
 
1278
1273
  // Check if metadata is valid and tblId matches
1279
1274
  if (storedLineMetadata && typeof storedLineMetadata.cols === 'number' && storedLineMetadata.tblId === lastClick.tblId) {
1280
- // log(`${logPrefix} Stored click info VALIDATED (Metadata OK and tblId matches). Trusting stored state.`);
1275
+ // log(`${logPrefix} Stored click info VALIDATED (Metadata OK and tblId matches). Trusting stored state.`);
1281
1276
  trustedLastClick = true;
1282
1277
  currentLineNum = lastClick.lineNum;
1283
1278
  targetCellIndex = lastClick.cellIndex;
@@ -1286,10 +1281,10 @@ exports.aceKeyEvent = (h, ctx) => {
1286
1281
 
1287
1282
  lineText = rep.lines.atIndex(currentLineNum)?.text || '';
1288
1283
  cellTexts = lineText.split(DELIMITER);
1289
- // log(`${logPrefix} Using Line=${currentLineNum}, CellIndex=${targetCellIndex}. Text: "${lineText}"`);
1284
+ // log(`${logPrefix} Using Line=${currentLineNum}, CellIndex=${targetCellIndex}. Text: "${lineText}"`);
1290
1285
 
1291
1286
  if (cellTexts.length !== metadataForTargetLine.cols) {
1292
- // log(`${logPrefix} WARNING: Stored cell count mismatch for trusted line ${currentLineNum}.`);
1287
+ // log(`${logPrefix} WARNING: Stored cell count mismatch for trusted line ${currentLineNum}.`);
1293
1288
  }
1294
1289
 
1295
1290
  cellStartCol = 0;
@@ -1297,20 +1292,20 @@ exports.aceKeyEvent = (h, ctx) => {
1297
1292
  cellStartCol += (cellTexts[i]?.length ?? 0) + DELIMITER.length;
1298
1293
  }
1299
1294
  precedingCellsOffset = cellStartCol;
1300
- // log(`${logPrefix} Calculated cellStartCol=${cellStartCol} from trusted cellIndex=${targetCellIndex}.`);
1295
+ // log(`${logPrefix} Calculated cellStartCol=${cellStartCol} from trusted cellIndex=${targetCellIndex}.`);
1301
1296
 
1302
1297
  if (typeof lastClick.relativePos === 'number' && lastClick.relativePos >= 0) {
1303
1298
  const currentCellTextLength = cellTexts[targetCellIndex]?.length ?? 0;
1304
1299
  relativeCaretPos = Math.max(0, Math.min(lastClick.relativePos, currentCellTextLength));
1305
- // log(`${logPrefix} Using and validated stored relative position: ${relativeCaretPos}.`);
1300
+ // log(`${logPrefix} Using and validated stored relative position: ${relativeCaretPos}.`);
1306
1301
  } else {
1307
1302
  relativeCaretPos = reportedCol - cellStartCol; // Use reportedCol for initial calc if relative is missing
1308
1303
  const currentCellTextLength = cellTexts[targetCellIndex]?.length ?? 0;
1309
1304
  relativeCaretPos = Math.max(0, Math.min(relativeCaretPos, currentCellTextLength));
1310
- // log(`${logPrefix} Stored relativePos missing, calculated from reportedCol (${reportedCol}): ${relativeCaretPos}`);
1305
+ // log(`${logPrefix} Stored relativePos missing, calculated from reportedCol (${reportedCol}): ${relativeCaretPos}`);
1311
1306
  }
1312
1307
  } else {
1313
- // log(`${logPrefix} Stored click info INVALID (Metadata missing/invalid or tblId mismatch). Clearing stored state.`);
1308
+ // log(`${logPrefix} Stored click info INVALID (Metadata missing/invalid or tblId mismatch). Clearing stored state.`);
1314
1309
  if (editor) editor.ep_data_tables_last_clicked = null;
1315
1310
  }
1316
1311
  } catch (e) {
@@ -1321,7 +1316,7 @@ exports.aceKeyEvent = (h, ctx) => {
1321
1316
 
1322
1317
  // ** Scenario 2: Fallback - Use reported line/col ONLY if stored info wasn't trusted **
1323
1318
  if (!trustedLastClick) {
1324
- // log(`${logPrefix} Fallback: Using reported caret position Line=${reportedLineNum}, Col=${reportedCol}.`);
1319
+ // log(`${logPrefix} Fallback: Using reported caret position Line=${reportedLineNum}, Col=${reportedCol}.`);
1325
1320
  // Fetch metadata for the reported line again, in case it wasn't fetched or was invalid earlier
1326
1321
  try {
1327
1322
  lineAttrString = docManager.getAttributeOnLine(reportedLineNum, ATTR_TABLE_JSON);
@@ -1338,11 +1333,11 @@ exports.aceKeyEvent = (h, ctx) => {
1338
1333
  if (tableInDOM) {
1339
1334
  const domTblId = tableInDOM.getAttribute('data-tblId');
1340
1335
  const domRow = tableInDOM.getAttribute('data-row');
1341
- // log(`${logPrefix} Fallback: Found table in DOM without attribute! TblId=${domTblId}, Row=${domRow}`);
1336
+ // log(`${logPrefix} Fallback: Found table in DOM without attribute! TblId=${domTblId}, Row=${domRow}`);
1342
1337
  // Try to reconstruct the metadata from DOM
1343
1338
  const domCells = tableInDOM.querySelectorAll('td');
1344
1339
  if (domTblId && domRow !== null && domCells.length > 0) {
1345
- // log(`${logPrefix} Fallback: Attempting to reconstruct metadata from DOM...`);
1340
+ // log(`${logPrefix} Fallback: Attempting to reconstruct metadata from DOM...`);
1346
1341
  const reconstructedMetadata = {
1347
1342
  tblId: domTblId,
1348
1343
  row: parseInt(domRow, 10),
@@ -1350,31 +1345,31 @@ exports.aceKeyEvent = (h, ctx) => {
1350
1345
  };
1351
1346
  lineAttrString = JSON.stringify(reconstructedMetadata);
1352
1347
  tableMetadata = reconstructedMetadata;
1353
- // log(`${logPrefix} Fallback: Reconstructed metadata: ${lineAttrString}`);
1348
+ // log(`${logPrefix} Fallback: Reconstructed metadata: ${lineAttrString}`);
1354
1349
  }
1355
1350
  }
1356
1351
  }
1357
1352
  } catch(e) {
1358
- // log(`${logPrefix} Fallback: Error checking DOM for table:`, e);
1353
+ // log(`${logPrefix} Fallback: Error checking DOM for table:`, e);
1359
1354
  }
1360
1355
  }
1361
1356
  } catch(e) { tableMetadata = null; } // Ignore errors here, handled below
1362
1357
 
1363
1358
  if (!tableMetadata) {
1364
- // log(`${logPrefix} Fallback: Reported line ${reportedLineNum} is not a valid table line. Allowing default.`);
1359
+ // log(`${logPrefix} Fallback: Reported line ${reportedLineNum} is not a valid table line. Allowing default.`);
1365
1360
  return false;
1366
1361
  }
1367
1362
 
1368
1363
  currentLineNum = reportedLineNum;
1369
1364
  metadataForTargetLine = tableMetadata;
1370
- // log(`${logPrefix} Fallback: Processing based on reported line ${currentLineNum}.`);
1365
+ // log(`${logPrefix} Fallback: Processing based on reported line ${currentLineNum}.`);
1371
1366
 
1372
1367
  lineText = rep.lines.atIndex(currentLineNum)?.text || '';
1373
1368
  cellTexts = lineText.split(DELIMITER);
1374
- // log(`${logPrefix} Fallback: Fetched text for reported line ${currentLineNum}: "${lineText}"`);
1369
+ // log(`${logPrefix} Fallback: Fetched text for reported line ${currentLineNum}: "${lineText}"`);
1375
1370
 
1376
1371
  if (cellTexts.length !== metadataForTargetLine.cols) {
1377
- // log(`${logPrefix} WARNING (Fallback): Cell count mismatch for reported line ${currentLineNum}.`);
1372
+ // log(`${logPrefix} WARNING (Fallback): Cell count mismatch for reported line ${currentLineNum}.`);
1378
1373
  }
1379
1374
 
1380
1375
  // Calculate target cell based on reportedCol
@@ -1388,7 +1383,7 @@ exports.aceKeyEvent = (h, ctx) => {
1388
1383
  relativeCaretPos = reportedCol - currentOffset;
1389
1384
  cellStartCol = currentOffset;
1390
1385
  precedingCellsOffset = cellStartCol;
1391
- // log(`${logPrefix} --> (Fallback Calc) Found target cell ${foundIndex}. RelativePos: ${relativeCaretPos}.`);
1386
+ // log(`${logPrefix} --> (Fallback Calc) Found target cell ${foundIndex}. RelativePos: ${relativeCaretPos}.`);
1392
1387
  break;
1393
1388
  }
1394
1389
  if (i < cellTexts.length - 1 && reportedCol === cellEndCol + DELIMITER.length) {
@@ -1396,7 +1391,7 @@ exports.aceKeyEvent = (h, ctx) => {
1396
1391
  relativeCaretPos = 0;
1397
1392
  cellStartCol = currentOffset + cellLength + DELIMITER.length;
1398
1393
  precedingCellsOffset = cellStartCol;
1399
- // log(`${logPrefix} --> (Fallback Calc) Caret at delimiter AFTER cell ${i}. Treating as start of cell ${foundIndex}.`);
1394
+ // log(`${logPrefix} --> (Fallback Calc) Caret at delimiter AFTER cell ${i}. Treating as start of cell ${foundIndex}.`);
1400
1395
  break;
1401
1396
  }
1402
1397
  currentOffset += cellLength + DELIMITER.length;
@@ -1409,9 +1404,9 @@ exports.aceKeyEvent = (h, ctx) => {
1409
1404
  for (let i = 0; i < foundIndex; i++) { cellStartCol += (cellTexts[i]?.length ?? 0) + DELIMITER.length; }
1410
1405
  precedingCellsOffset = cellStartCol;
1411
1406
  relativeCaretPos = cellTexts[foundIndex]?.length ?? 0;
1412
- // log(`${logPrefix} --> (Fallback Calc) Caret detected at END of last cell (${foundIndex}).`);
1407
+ // log(`${logPrefix} --> (Fallback Calc) Caret detected at END of last cell (${foundIndex}).`);
1413
1408
  } else {
1414
- // log(`${logPrefix} (Fallback Calc) FAILED to determine target cell for caret col ${reportedCol}. Allowing default handling.`);
1409
+ // log(`${logPrefix} (Fallback Calc) FAILED to determine target cell for caret col ${reportedCol}. Allowing default handling.`);
1415
1410
  return false;
1416
1411
  }
1417
1412
  }
@@ -1420,12 +1415,12 @@ exports.aceKeyEvent = (h, ctx) => {
1420
1415
 
1421
1416
  // --- Final Validation ---
1422
1417
  if (currentLineNum < 0 || targetCellIndex < 0 || !metadataForTargetLine || targetCellIndex >= metadataForTargetLine.cols) {
1423
- // log(`${logPrefix} FAILED final validation: Line=${currentLineNum}, Cell=${targetCellIndex}, Metadata=${!!metadataForTargetLine}. Allowing default.`);
1418
+ // log(`${logPrefix} FAILED final validation: Line=${currentLineNum}, Cell=${targetCellIndex}, Metadata=${!!metadataForTargetLine}. Allowing default.`);
1424
1419
  if (editor) editor.ep_data_tables_last_clicked = null;
1425
1420
  return false;
1426
1421
  }
1427
1422
 
1428
- // log(`${logPrefix} --> Final Target: Line=${currentLineNum}, CellIndex=${targetCellIndex}, RelativePos=${relativeCaretPos}`);
1423
+ // log(`${logPrefix} --> Final Target: Line=${currentLineNum}, CellIndex=${targetCellIndex}, RelativePos=${relativeCaretPos}`);
1429
1424
  // --- End Cell/Position Determination ---
1430
1425
 
1431
1426
  // --- START NEW: Handle Highlight Deletion/Replacement ---
@@ -1434,11 +1429,11 @@ exports.aceKeyEvent = (h, ctx) => {
1434
1429
  const hasSelection = selStartActual[0] !== selEndActual[0] || selStartActual[1] !== selEndActual[1];
1435
1430
 
1436
1431
  if (hasSelection) {
1437
- // log(`${logPrefix} [selection] Active selection detected. Start:[${selStartActual[0]},${selStartActual[1]}], End:[${selEndActual[0]},${selEndActual[1]}]`);
1438
- // log(`${logPrefix} [caretTrace] [selection] Initial rep.selStart: Line=${rep.selStart[0]}, Col=${rep.selStart[1]}`);
1432
+ // log(`${logPrefix} [selection] Active selection detected. Start:[${selStartActual[0]},${selStartActual[1]}], End:[${selEndActual[0]},${selEndActual[1]}]`);
1433
+ // log(`${logPrefix} [caretTrace] [selection] Initial rep.selStart: Line=${rep.selStart[0]}, Col=${rep.selStart[1]}`);
1439
1434
 
1440
1435
  if (selStartActual[0] !== currentLineNum || selEndActual[0] !== currentLineNum) {
1441
- // log(`${logPrefix} [selection] Selection spans multiple lines (${selStartActual[0]}-${selEndActual[0]}) or is not on the current focused table line (${currentLineNum}). Preventing default action.`);
1436
+ // log(`${logPrefix} [selection] Selection spans multiple lines (${selStartActual[0]}-${selEndActual[0]}) or is not on the current focused table line (${currentLineNum}). Preventing default action.`);
1442
1437
  evt.preventDefault();
1443
1438
  return true;
1444
1439
  }
@@ -1486,7 +1481,7 @@ exports.aceKeyEvent = (h, ctx) => {
1486
1481
  selectionEndColInLine = cellContentEndColInLine;
1487
1482
  }
1488
1483
 
1489
- // log(`${logPrefix} [selection] Cell context for selection: targetCellIndex=${targetCellIndex}, cellStartColInLine=${cellContentStartColInLine}, cellEndColInLine=${cellContentEndColInLine}, currentCellFullText='${currentCellFullText}'`);
1484
+ // log(`${logPrefix} [selection] Cell context for selection: targetCellIndex=${targetCellIndex}, cellStartColInLine=${cellContentStartColInLine}, cellEndColInLine=${cellContentEndColInLine}, currentCellFullText='${currentCellFullText}'`);
1490
1485
 
1491
1486
  const isSelectionEntirelyWithinCell =
1492
1487
  selectionStartColInLine >= cellContentStartColInLine &&
@@ -1518,10 +1513,10 @@ exports.aceKeyEvent = (h, ctx) => {
1518
1513
 
1519
1514
 
1520
1515
  if (isSelectionEntirelyWithinCell && (isCurrentKeyDelete || isCurrentKeyBackspace || isCurrentKeyTyping)) {
1521
- // log(`${logPrefix} [selection] Handling key='${evt.key}' (Type: ${evt.type}) for valid intra-cell selection.`);
1516
+ // log(`${logPrefix} [selection] Handling key='${evt.key}' (Type: ${evt.type}) for valid intra-cell selection.`);
1522
1517
 
1523
1518
  if (evt.type !== 'keydown') {
1524
- // log(`${logPrefix} [selection] Ignoring non-keydown event type ('${evt.type}') for selection handling. Allowing default.`);
1519
+ // log(`${logPrefix} [selection] Ignoring non-keydown event type ('${evt.type}') for selection handling. Allowing default.`);
1525
1520
  return false;
1526
1521
  }
1527
1522
  evt.preventDefault();
@@ -1531,20 +1526,20 @@ exports.aceKeyEvent = (h, ctx) => {
1531
1526
  let replacementText = '';
1532
1527
  let newAbsoluteCaretCol = selectionStartColInLine;
1533
1528
  const repBeforeEdit = editorInfo.ace_getRep(); // Get rep before edit for attribute helper
1534
- // log(`${logPrefix} [caretTrace] [selection] rep.selStart before ace_performDocumentReplaceRange: Line=${repBeforeEdit.selStart[0]}, Col=${repBeforeEdit.selStart[1]}`);
1529
+ // log(`${logPrefix} [caretTrace] [selection] rep.selStart before ace_performDocumentReplaceRange: Line=${repBeforeEdit.selStart[0]}, Col=${repBeforeEdit.selStart[1]}`);
1535
1530
 
1536
1531
  if (isCurrentKeyTyping) {
1537
1532
  replacementText = evt.key;
1538
1533
  newAbsoluteCaretCol = selectionStartColInLine + replacementText.length;
1539
- // log(`${logPrefix} [selection] -> Replacing selected range [[${rangeStart[0]},${rangeStart[1]}],[${rangeEnd[0]},${rangeEnd[1]}]] with text '${replacementText}'`);
1534
+ // log(`${logPrefix} [selection] -> Replacing selected range [[${rangeStart[0]},${rangeStart[1]}],[${rangeEnd[0]},${rangeEnd[1]}]] with text '${replacementText}'`);
1540
1535
  } else { // Delete or Backspace
1541
- // log(`${logPrefix} [selection] -> Deleting selected range [[${rangeStart[0]},${rangeStart[1]}],[${rangeEnd[0]},${rangeEnd[1]}]]`);
1536
+ // log(`${logPrefix} [selection] -> Deleting selected range [[${rangeStart[0]},${rangeStart[1]}],[${rangeEnd[0]},${rangeEnd[1]}]]`);
1542
1537
  // If whole cell is being wiped, keep a single space so cell isn't empty
1543
1538
  const isWholeCell = selectionStartColInLine <= cellContentStartColInLine && selectionEndColInLine >= cellContentEndColInLine;
1544
1539
  if (isWholeCell) {
1545
1540
  replacementText = ' ';
1546
1541
  newAbsoluteCaretCol = selectionStartColInLine + 1;
1547
- // log(`${logPrefix} [selection] Whole cell cleared – inserting single space to preserve caret/author span.`);
1542
+ // log(`${logPrefix} [selection] Whole cell cleared – inserting single space to preserve caret/author span.`);
1548
1543
  }
1549
1544
  }
1550
1545
 
@@ -1563,44 +1558,44 @@ exports.aceKeyEvent = (h, ctx) => {
1563
1558
  );
1564
1559
  }
1565
1560
  const repAfterReplace = editorInfo.ace_getRep();
1566
- // log(`${logPrefix} [caretTrace] [selection] rep.selStart after ace_performDocumentReplaceRange: Line=${repAfterReplace.selStart[0]}, Col=${repAfterReplace.selStart[1]}`);
1561
+ // log(`${logPrefix} [caretTrace] [selection] rep.selStart after ace_performDocumentReplaceRange: Line=${repAfterReplace.selStart[0]}, Col=${repAfterReplace.selStart[1]}`);
1567
1562
 
1568
1563
 
1569
- // log(`${logPrefix} [selection] -> Re-applying tbljson line attribute...`);
1564
+ // log(`${logPrefix} [selection] -> Re-applying tbljson line attribute...`);
1570
1565
  const applyHelper = editorInfo.ep_data_tables_applyMeta;
1571
1566
  if (applyHelper && typeof applyHelper === 'function' && repBeforeEdit) {
1572
1567
  const attrStringToApply = (trustedLastClick || reportedLineNum === currentLineNum) ? lineAttrString : null;
1573
1568
  applyHelper(currentLineNum, metadataForTargetLine.tblId, metadataForTargetLine.row, metadataForTargetLine.cols, repBeforeEdit, editorInfo, attrStringToApply, docManager);
1574
- // log(`${logPrefix} [selection] -> tbljson line attribute re-applied (using rep before edit).`);
1569
+ // log(`${logPrefix} [selection] -> tbljson line attribute re-applied (using rep before edit).`);
1575
1570
  } else {
1576
1571
  console.error(`${logPrefix} [selection] -> FAILED to re-apply tbljson attribute (helper or repBeforeEdit missing).`);
1577
1572
  const currentRepFallback = editorInfo.ace_getRep();
1578
1573
  if (applyHelper && typeof applyHelper === 'function' && currentRepFallback) {
1579
- // log(`${logPrefix} [selection] -> Retrying attribute application with current rep...`);
1574
+ // log(`${logPrefix} [selection] -> Retrying attribute application with current rep...`);
1580
1575
  applyHelper(currentLineNum, metadataForTargetLine.tblId, metadataForTargetLine.row, metadataForTargetLine.cols, currentRepFallback, editorInfo, null, docManager);
1581
- // log(`${logPrefix} [selection] -> tbljson line attribute re-applied (using current rep fallback).`);
1576
+ // log(`${logPrefix} [selection] -> tbljson line attribute re-applied (using current rep fallback).`);
1582
1577
  } else {
1583
1578
  console.error(`${logPrefix} [selection] -> FAILED to re-apply tbljson attribute even with fallback rep.`);
1584
1579
  }
1585
1580
  }
1586
1581
 
1587
- // log(`${logPrefix} [selection] -> Setting selection/caret to: [${currentLineNum}, ${newAbsoluteCaretCol}]`);
1588
- // log(`${logPrefix} [caretTrace] [selection] rep.selStart before ace_performSelectionChange: Line=${editorInfo.ace_getRep().selStart[0]}, Col=${editorInfo.ace_getRep().selStart[1]}`);
1582
+ // log(`${logPrefix} [selection] -> Setting selection/caret to: [${currentLineNum}, ${newAbsoluteCaretCol}]`);
1583
+ // log(`${logPrefix} [caretTrace] [selection] rep.selStart before ace_performSelectionChange: Line=${editorInfo.ace_getRep().selStart[0]}, Col=${editorInfo.ace_getRep().selStart[1]}`);
1589
1584
  editorInfo.ace_performSelectionChange([currentLineNum, newAbsoluteCaretCol], [currentLineNum, newAbsoluteCaretCol], false);
1590
1585
  const repAfterSelectionChange = editorInfo.ace_getRep();
1591
- // log(`${logPrefix} [caretTrace] [selection] rep.selStart after ace_performSelectionChange: Line=${repAfterSelectionChange.selStart[0]}, Col=${repAfterSelectionChange.selStart[1]}`);
1586
+ // log(`${logPrefix} [caretTrace] [selection] rep.selStart after ace_performSelectionChange: Line=${repAfterSelectionChange.selStart[0]}, Col=${repAfterSelectionChange.selStart[1]}`);
1592
1587
 
1593
1588
  // Add sync hint AFTER setting selection
1594
1589
  editorInfo.ace_fastIncorp(1);
1595
1590
  const repAfterFastIncorp = editorInfo.ace_getRep();
1596
- // log(`${logPrefix} [caretTrace] [selection] rep.selStart after ace_fastIncorp: Line=${repAfterFastIncorp.selStart[0]}, Col=${repAfterFastIncorp.selStart[1]}`);
1597
- // log(`${logPrefix} [selection] -> Requested sync hint (fastIncorp 1).`);
1591
+ // log(`${logPrefix} [caretTrace] [selection] rep.selStart after ace_fastIncorp: Line=${repAfterFastIncorp.selStart[0]}, Col=${repAfterFastIncorp.selStart[1]}`);
1592
+ // log(`${logPrefix} [selection] -> Requested sync hint (fastIncorp 1).`);
1598
1593
 
1599
1594
  // --- Re-assert selection ---
1600
- // log(`${logPrefix} [caretTrace] [selection] Attempting to re-assert selection post-fastIncorp to [${currentLineNum}, ${newAbsoluteCaretCol}]`);
1595
+ // log(`${logPrefix} [caretTrace] [selection] Attempting to re-assert selection post-fastIncorp to [${currentLineNum}, ${newAbsoluteCaretCol}]`);
1601
1596
  editorInfo.ace_performSelectionChange([currentLineNum, newAbsoluteCaretCol], [currentLineNum, newAbsoluteCaretCol], false);
1602
1597
  const repAfterReassert = editorInfo.ace_getRep();
1603
- // log(`${logPrefix} [caretTrace] [selection] rep.selStart after re-asserting selection: Line=${repAfterReassert.selStart[0]}, Col=${repAfterReassert.selStart[1]}`);
1598
+ // log(`${logPrefix} [caretTrace] [selection] rep.selStart after re-asserting selection: Line=${repAfterReassert.selStart[0]}, Col=${repAfterReassert.selStart[1]}`);
1604
1599
 
1605
1600
  const newRelativePos = newAbsoluteCaretCol - cellStartCol;
1606
1601
  if (editor) {
@@ -1610,15 +1605,15 @@ exports.aceKeyEvent = (h, ctx) => {
1610
1605
  cellIndex: targetCellIndex,
1611
1606
  relativePos: newRelativePos < 0 ? 0 : newRelativePos
1612
1607
  };
1613
- // log(`${logPrefix} [selection] -> Updated stored click/caret info:`, editor.ep_data_tables_last_clicked);
1608
+ // log(`${logPrefix} [selection] -> Updated stored click/caret info:`, editor.ep_data_tables_last_clicked);
1614
1609
  } else {
1615
- // log(`${logPrefix} [selection] -> Editor instance not found, cannot update ep_data_tables_last_clicked.`);
1610
+ // log(`${logPrefix} [selection] -> Editor instance not found, cannot update ep_data_tables_last_clicked.`);
1616
1611
  }
1617
1612
 
1618
- // log(`${logPrefix} END [selection] (Handled highlight modification) Key='${evt.key}' Type='${evt.type}'. Duration: ${Date.now() - startLogTime}ms`);
1613
+ // log(`${logPrefix} END [selection] (Handled highlight modification) Key='${evt.key}' Type='${evt.type}'. Duration: ${Date.now() - startLogTime}ms`);
1619
1614
  return true;
1620
1615
  } catch (error) {
1621
- // log(`${logPrefix} [selection] ERROR during highlight modification:`, error);
1616
+ // log(`${logPrefix} [selection] ERROR during highlight modification:`, error);
1622
1617
  console.error('[ep_data_tables] Error processing highlight modification:', error);
1623
1618
  return true; // Still return true as we prevented default.
1624
1619
  }
@@ -1629,12 +1624,12 @@ exports.aceKeyEvent = (h, ctx) => {
1629
1624
  // --- Check for Ctrl+X (Cut) key combination ---
1630
1625
  const isCutKey = (evt.ctrlKey || evt.metaKey) && (evt.key === 'x' || evt.key === 'X' || evt.keyCode === 88);
1631
1626
  if (isCutKey && hasSelection) {
1632
- log(`${logPrefix} Ctrl+X (Cut) detected with selection. Letting cut event handler manage this.`);
1627
+ // log(`${logPrefix} Ctrl+X (Cut) detected with selection. Letting cut event handler manage this.`);
1633
1628
  // Let the cut event handler handle this - we don't need to preventDefault here
1634
1629
  // as the cut event will handle the operation and prevent default
1635
1630
  return false; // Allow the cut event to be triggered
1636
1631
  } else if (isCutKey && !hasSelection) {
1637
- log(`${logPrefix} Ctrl+X (Cut) detected but no selection. Allowing default.`);
1632
+ // log(`${logPrefix} Ctrl+X (Cut) detected but no selection. Allowing default.`);
1638
1633
  return false; // Allow default - nothing to cut
1639
1634
  }
1640
1635
 
@@ -1645,7 +1640,7 @@ exports.aceKeyEvent = (h, ctx) => {
1645
1640
  const isNavigationKey = [33, 34, 35, 36, 37, 38, 39, 40].includes(evt.keyCode);
1646
1641
  const isTabKey = evt.key === 'Tab';
1647
1642
  const isEnterKey = evt.key === 'Enter';
1648
- // log(`${logPrefix} Key classification: Typing=${isTypingKey}, Backspace=${isBackspaceKey}, Delete=${isDeleteKey}, Nav=${isNavigationKey}, Tab=${isTabKey}, Enter=${isEnterKey}, Cut=${isCutKey}`);
1643
+ // log(`${logPrefix} Key classification: Typing=${isTypingKey}, Backspace=${isBackspaceKey}, Delete=${isDeleteKey}, Nav=${isNavigationKey}, Tab=${isTabKey}, Enter=${isEnterKey}, Cut=${isCutKey}`);
1649
1644
 
1650
1645
  /*
1651
1646
  * Prevent caret placement *after* the invisible caret-anchor.
@@ -1660,7 +1655,7 @@ exports.aceKeyEvent = (h, ctx) => {
1660
1655
  if (evt.type === 'keydown' && !evt.ctrlKey && !evt.metaKey && !evt.altKey) {
1661
1656
  // Right-arrow – if at the very end of a cell, move to the next cell.
1662
1657
  if (evt.keyCode === 39 && relativeCaretPos >= currentCellTextLengthEarly && targetCellIndex < metadataForTargetLine.cols - 1) {
1663
- // log(`${logPrefix} ArrowRight at cell boundary – navigating to next cell to avoid anchor zone.`);
1658
+ // log(`${logPrefix} ArrowRight at cell boundary – navigating to next cell to avoid anchor zone.`);
1664
1659
  evt.preventDefault();
1665
1660
  navigateToNextCell(currentLineNum, targetCellIndex, metadataForTargetLine, false, editorInfo, docManager);
1666
1661
  return true;
@@ -1668,7 +1663,7 @@ exports.aceKeyEvent = (h, ctx) => {
1668
1663
 
1669
1664
  // Left-arrow – if at the very start of a cell, move to the previous cell.
1670
1665
  if (evt.keyCode === 37 && relativeCaretPos === 0 && targetCellIndex > 0) {
1671
- // log(`${logPrefix} ArrowLeft at cell boundary – navigating to previous cell to avoid anchor zone.`);
1666
+ // log(`${logPrefix} ArrowLeft at cell boundary – navigating to previous cell to avoid anchor zone.`);
1672
1667
  evt.preventDefault();
1673
1668
  navigateToNextCell(currentLineNum, targetCellIndex, metadataForTargetLine, true, editorInfo, docManager);
1674
1669
  return true;
@@ -1679,45 +1674,45 @@ exports.aceKeyEvent = (h, ctx) => {
1679
1674
 
1680
1675
  // 1. Allow non-Tab navigation keys immediately
1681
1676
  if (isNavigationKey && !isTabKey) {
1682
- // log(`${logPrefix} Allowing navigation key: ${evt.key}. Clearing click state.`);
1677
+ // log(`${logPrefix} Allowing navigation key: ${evt.key}. Clearing click state.`);
1683
1678
  if (editor) editor.ep_data_tables_last_clicked = null; // Clear state on navigation
1684
1679
  return false;
1685
1680
  }
1686
1681
 
1687
1682
  // 2. Handle Tab - Navigate to next cell (only on keydown to avoid double navigation)
1688
1683
  if (isTabKey) {
1689
- // log(`${logPrefix} Tab key pressed. Event type: ${evt.type}`);
1684
+ // log(`${logPrefix} Tab key pressed. Event type: ${evt.type}`);
1690
1685
  evt.preventDefault();
1691
1686
 
1692
1687
  // Only process keydown events for navigation to avoid double navigation
1693
1688
  if (evt.type !== 'keydown') {
1694
- // log(`${logPrefix} Ignoring Tab ${evt.type} event to prevent double navigation.`);
1689
+ // log(`${logPrefix} Ignoring Tab ${evt.type} event to prevent double navigation.`);
1695
1690
  return true;
1696
1691
  }
1697
1692
 
1698
- // log(`${logPrefix} Processing Tab keydown - implementing cell navigation.`);
1693
+ // log(`${logPrefix} Processing Tab keydown - implementing cell navigation.`);
1699
1694
  const success = navigateToNextCell(currentLineNum, targetCellIndex, metadataForTargetLine, evt.shiftKey, editorInfo, docManager);
1700
1695
  if (!success) {
1701
- // log(`${logPrefix} Tab navigation failed, cell navigation not possible.`);
1696
+ // log(`${logPrefix} Tab navigation failed, cell navigation not possible.`);
1702
1697
  }
1703
1698
  return true;
1704
1699
  }
1705
1700
 
1706
1701
  // 3. Handle Enter - Navigate to cell below (only on keydown to avoid double navigation)
1707
1702
  if (isEnterKey) {
1708
- // log(`${logPrefix} Enter key pressed. Event type: ${evt.type}`);
1703
+ // log(`${logPrefix} Enter key pressed. Event type: ${evt.type}`);
1709
1704
  evt.preventDefault();
1710
1705
 
1711
1706
  // Only process keydown events for navigation to avoid double navigation
1712
1707
  if (evt.type !== 'keydown') {
1713
- // log(`${logPrefix} Ignoring Enter ${evt.type} event to prevent double navigation.`);
1708
+ // log(`${logPrefix} Ignoring Enter ${evt.type} event to prevent double navigation.`);
1714
1709
  return true;
1715
1710
  }
1716
1711
 
1717
- // log(`${logPrefix} Processing Enter keydown - implementing cell navigation.`);
1712
+ // log(`${logPrefix} Processing Enter keydown - implementing cell navigation.`);
1718
1713
  const success = navigateToCellBelow(currentLineNum, targetCellIndex, metadataForTargetLine, editorInfo, docManager);
1719
1714
  if (!success) {
1720
- // log(`${logPrefix} Enter navigation failed, cell navigation not possible.`);
1715
+ // log(`${logPrefix} Enter navigation failed, cell navigation not possible.`);
1721
1716
  }
1722
1717
  return true;
1723
1718
  }
@@ -1726,25 +1721,25 @@ exports.aceKeyEvent = (h, ctx) => {
1726
1721
  const currentCellTextLength = cellTexts[targetCellIndex]?.length ?? 0;
1727
1722
  // Backspace at the very beginning of cell > 0
1728
1723
  if (isBackspaceKey && relativeCaretPos === 0 && targetCellIndex > 0) {
1729
- // log(`${logPrefix} Intercepted Backspace at start of cell ${targetCellIndex}. Preventing default.`);
1724
+ // log(`${logPrefix} Intercepted Backspace at start of cell ${targetCellIndex}. Preventing default.`);
1730
1725
  evt.preventDefault();
1731
1726
  return true;
1732
1727
  }
1733
1728
  // NEW: Backspace at very beginning of first cell – would merge with previous line
1734
1729
  if (isBackspaceKey && relativeCaretPos === 0 && targetCellIndex === 0) {
1735
- // log(`${logPrefix} Intercepted Backspace at start of first cell (line boundary). Preventing merge.`);
1730
+ // log(`${logPrefix} Intercepted Backspace at start of first cell (line boundary). Preventing merge.`);
1736
1731
  evt.preventDefault();
1737
1732
  return true;
1738
1733
  }
1739
1734
  // Delete at the very end of cell < last cell
1740
1735
  if (isDeleteKey && relativeCaretPos === currentCellTextLength && targetCellIndex < metadataForTargetLine.cols - 1) {
1741
- // log(`${logPrefix} Intercepted Delete at end of cell ${targetCellIndex}. Preventing default.`);
1736
+ // log(`${logPrefix} Intercepted Delete at end of cell ${targetCellIndex}. Preventing default.`);
1742
1737
  evt.preventDefault();
1743
1738
  return true;
1744
1739
  }
1745
1740
  // NEW: Delete at very end of last cell – would merge with next line
1746
1741
  if (isDeleteKey && relativeCaretPos === currentCellTextLength && targetCellIndex === metadataForTargetLine.cols - 1) {
1747
- // log(`${logPrefix} Intercepted Delete at end of last cell (line boundary). Preventing merge.`);
1742
+ // log(`${logPrefix} Intercepted Delete at end of last cell (line boundary). Preventing merge.`);
1748
1743
  evt.preventDefault();
1749
1744
  return true;
1750
1745
  }
@@ -1756,7 +1751,7 @@ exports.aceKeyEvent = (h, ctx) => {
1756
1751
  // Guard: internal Backspace at relativePos 1 (would delete delimiter) & Delete at relativePos 0
1757
1752
  if ((isInternalBackspace && relativeCaretPos === 1 && targetCellIndex > 0) ||
1758
1753
  (isInternalDelete && relativeCaretPos === 0 && targetCellIndex > 0)) {
1759
- // log(`${logPrefix} Attempt to erase protected delimiter – operation blocked.`);
1754
+ // log(`${logPrefix} Attempt to erase protected delimiter – operation blocked.`);
1760
1755
  evt.preventDefault();
1761
1756
  return true;
1762
1757
  }
@@ -1764,25 +1759,25 @@ exports.aceKeyEvent = (h, ctx) => {
1764
1759
  if (isTypingKey || isInternalBackspace || isInternalDelete) {
1765
1760
  // --- PREVENT TYPING DIRECTLY AFTER DELIMITER (relativeCaretPos===0) ---
1766
1761
  if (isTypingKey && relativeCaretPos === 0 && targetCellIndex > 0) {
1767
- // log(`${logPrefix} Caret at forbidden position 0 (just after delimiter). Auto-advancing to position 1.`);
1762
+ // log(`${logPrefix} Caret at forbidden position 0 (just after delimiter). Auto-advancing to position 1.`);
1768
1763
  const safePosAbs = cellStartCol + 1;
1769
1764
  editorInfo.ace_performSelectionChange([currentLineNum, safePosAbs], [currentLineNum, safePosAbs], false);
1770
1765
  editorInfo.ace_updateBrowserSelectionFromRep();
1771
1766
  relativeCaretPos = 1;
1772
- // log(`${logPrefix} Caret moved to safe position. New relativeCaretPos=${relativeCaretPos}`);
1767
+ // log(`${logPrefix} Caret moved to safe position. New relativeCaretPos=${relativeCaretPos}`);
1773
1768
  }
1774
1769
  // *** Use the validated currentLineNum and currentCol derived from relativeCaretPos ***
1775
1770
  const currentCol = cellStartCol + relativeCaretPos;
1776
- // log(`${logPrefix} Handling INTERNAL key='${evt.key}' Type='${evt.type}' at Line=${currentLineNum}, Col=${currentCol} (CellIndex=${targetCellIndex}, RelativePos=${relativeCaretPos}).`);
1777
- // log(`${logPrefix} [caretTrace] Initial rep.selStart for internal edit: Line=${rep.selStart[0]}, Col=${rep.selStart[1]}`);
1771
+ // log(`${logPrefix} Handling INTERNAL key='${evt.key}' Type='${evt.type}' at Line=${currentLineNum}, Col=${currentCol} (CellIndex=${targetCellIndex}, RelativePos=${relativeCaretPos}).`);
1772
+ // log(`${logPrefix} [caretTrace] Initial rep.selStart for internal edit: Line=${rep.selStart[0]}, Col=${rep.selStart[1]}`);
1778
1773
 
1779
1774
  // Only process keydown events for modifications
1780
1775
  if (evt.type !== 'keydown') {
1781
- // log(`${logPrefix} Ignoring non-keydown event type ('${evt.type}') for handled key.`);
1776
+ // log(`${logPrefix} Ignoring non-keydown event type ('${evt.type}') for handled key.`);
1782
1777
  return false;
1783
1778
  }
1784
1779
 
1785
- // log(`${logPrefix} Preventing default browser action for keydown event.`);
1780
+ // log(`${logPrefix} Preventing default browser action for keydown event.`);
1786
1781
  evt.preventDefault();
1787
1782
 
1788
1783
  let newAbsoluteCaretCol = -1;
@@ -1790,56 +1785,56 @@ exports.aceKeyEvent = (h, ctx) => {
1790
1785
 
1791
1786
  try {
1792
1787
  repBeforeEdit = editorInfo.ace_getRep(); // Get rep *before* making changes
1793
- // log(`${logPrefix} [caretTrace] rep.selStart before ace_performDocumentReplaceRange: Line=${repBeforeEdit.selStart[0]}, Col=${repBeforeEdit.selStart[1]}`);
1788
+ // log(`${logPrefix} [caretTrace] rep.selStart before ace_performDocumentReplaceRange: Line=${repBeforeEdit.selStart[0]}, Col=${repBeforeEdit.selStart[1]}`);
1794
1789
 
1795
1790
  if (isTypingKey) {
1796
1791
  const insertPos = [currentLineNum, currentCol];
1797
- // log(`${logPrefix} -> Inserting text '${evt.key}' at [${insertPos}]`);
1792
+ // log(`${logPrefix} -> Inserting text '${evt.key}' at [${insertPos}]`);
1798
1793
  editorInfo.ace_performDocumentReplaceRange(insertPos, insertPos, evt.key);
1799
1794
  newAbsoluteCaretCol = currentCol + 1;
1800
1795
 
1801
1796
  } else if (isInternalBackspace) {
1802
1797
  const delRangeStart = [currentLineNum, currentCol - 1];
1803
1798
  const delRangeEnd = [currentLineNum, currentCol];
1804
- // log(`${logPrefix} -> Deleting (Backspace) range [${delRangeStart}]-[${delRangeEnd}]`);
1799
+ // log(`${logPrefix} -> Deleting (Backspace) range [${delRangeStart}]-[${delRangeEnd}]`);
1805
1800
  editorInfo.ace_performDocumentReplaceRange(delRangeStart, delRangeEnd, '');
1806
1801
  newAbsoluteCaretCol = currentCol - 1;
1807
1802
 
1808
1803
  } else if (isInternalDelete) {
1809
1804
  const delRangeStart = [currentLineNum, currentCol];
1810
1805
  const delRangeEnd = [currentLineNum, currentCol + 1];
1811
- // log(`${logPrefix} -> Deleting (Delete) range [${delRangeStart}]-[${delRangeEnd}]`);
1806
+ // log(`${logPrefix} -> Deleting (Delete) range [${delRangeStart}]-[${delRangeEnd}]`);
1812
1807
  editorInfo.ace_performDocumentReplaceRange(delRangeStart, delRangeEnd, '');
1813
1808
  newAbsoluteCaretCol = currentCol; // Caret stays at the same column for delete
1814
1809
  }
1815
1810
  const repAfterReplace = editorInfo.ace_getRep();
1816
- // log(`${logPrefix} [caretTrace] rep.selStart after ace_performDocumentReplaceRange: Line=${repAfterReplace.selStart[0]}, Col=${repAfterReplace.selStart[1]}`);
1811
+ // log(`${logPrefix} [caretTrace] rep.selStart after ace_performDocumentReplaceRange: Line=${repAfterReplace.selStart[0]}, Col=${repAfterReplace.selStart[1]}`);
1817
1812
 
1818
1813
 
1819
1814
  // *** CRITICAL: Re-apply the line attribute after ANY modification ***
1820
- // log(`${logPrefix} -> Re-applying tbljson line attribute...`);
1815
+ // log(`${logPrefix} -> Re-applying tbljson line attribute...`);
1821
1816
 
1822
1817
  // DEBUG: Log the values before calculating attrStringToApply
1823
- // log(`${logPrefix} DEBUG: Before calculating attrStringToApply - trustedLastClick=${trustedLastClick}, reportedLineNum=${reportedLineNum}, currentLineNum=${currentLineNum}`);
1824
- // log(`${logPrefix} DEBUG: lineAttrString value:`, lineAttrString ? `"${lineAttrString}"` : 'null/undefined');
1818
+ // log(`${logPrefix} DEBUG: Before calculating attrStringToApply - trustedLastClick=${trustedLastClick}, reportedLineNum=${reportedLineNum}, currentLineNum=${currentLineNum}`);
1819
+ // log(`${logPrefix} DEBUG: lineAttrString value:`, lineAttrString ? `"${lineAttrString}"` : 'null/undefined');
1825
1820
 
1826
1821
  const applyHelper = editorInfo.ep_data_tables_applyMeta;
1827
1822
  if (applyHelper && typeof applyHelper === 'function' && repBeforeEdit) {
1828
1823
  // Pass the original lineAttrString if available AND if it belongs to the currentLineNum
1829
1824
  const attrStringToApply = (trustedLastClick || reportedLineNum === currentLineNum) ? lineAttrString : null;
1830
1825
 
1831
- // log(`${logPrefix} DEBUG: Calculated attrStringToApply:`, attrStringToApply ? `"${attrStringToApply}"` : 'null/undefined');
1832
- // log(`${logPrefix} DEBUG: Condition result: (${trustedLastClick} || ${reportedLineNum} === ${currentLineNum}) = ${trustedLastClick || reportedLineNum === currentLineNum}`);
1826
+ // log(`${logPrefix} DEBUG: Calculated attrStringToApply:`, attrStringToApply ? `"${attrStringToApply}"` : 'null/undefined');
1827
+ // log(`${logPrefix} DEBUG: Condition result: (${trustedLastClick} || ${reportedLineNum} === ${currentLineNum}) = ${trustedLastClick || reportedLineNum === currentLineNum}`);
1833
1828
 
1834
1829
  applyHelper(currentLineNum, metadataForTargetLine.tblId, metadataForTargetLine.row, metadataForTargetLine.cols, repBeforeEdit, editorInfo, attrStringToApply, docManager);
1835
- // log(`${logPrefix} -> tbljson line attribute re-applied (using rep before edit).`);
1830
+ // log(`${logPrefix} -> tbljson line attribute re-applied (using rep before edit).`);
1836
1831
  } else {
1837
1832
  console.error(`${logPrefix} -> FAILED to re-apply tbljson attribute (helper or repBeforeEdit missing).`);
1838
1833
  const currentRepFallback = editorInfo.ace_getRep();
1839
1834
  if (applyHelper && typeof applyHelper === 'function' && currentRepFallback) {
1840
- // log(`${logPrefix} -> Retrying attribute application with current rep...`);
1835
+ // log(`${logPrefix} -> Retrying attribute application with current rep...`);
1841
1836
  applyHelper(currentLineNum, metadataForTargetLine.tblId, metadataForTargetLine.row, metadataForTargetLine.cols, currentRepFallback, editorInfo, null, docManager); // Cannot guarantee old attr string is valid here
1842
- // log(`${logPrefix} -> tbljson line attribute re-applied (using current rep fallback).`);
1837
+ // log(`${logPrefix} -> tbljson line attribute re-applied (using current rep fallback).`);
1843
1838
  } else {
1844
1839
  console.error(`${logPrefix} -> FAILED to re-apply tbljson attribute even with fallback rep.`);
1845
1840
  }
@@ -1848,26 +1843,26 @@ exports.aceKeyEvent = (h, ctx) => {
1848
1843
  // Set caret position immediately
1849
1844
  if (newAbsoluteCaretCol >= 0) {
1850
1845
  const newCaretPos = [currentLineNum, newAbsoluteCaretCol]; // Use the trusted currentLineNum
1851
- // log(`${logPrefix} -> Setting selection immediately to:`, newCaretPos);
1852
- // log(`${logPrefix} [caretTrace] rep.selStart before ace_performSelectionChange: Line=${editorInfo.ace_getRep().selStart[0]}, Col=${editorInfo.ace_getRep().selStart[1]}`);
1846
+ // log(`${logPrefix} -> Setting selection immediately to:`, newCaretPos);
1847
+ // log(`${logPrefix} [caretTrace] rep.selStart before ace_performSelectionChange: Line=${editorInfo.ace_getRep().selStart[0]}, Col=${editorInfo.ace_getRep().selStart[1]}`);
1853
1848
  try {
1854
1849
  editorInfo.ace_performSelectionChange(newCaretPos, newCaretPos, false);
1855
1850
  const repAfterSelectionChange = editorInfo.ace_getRep();
1856
- // log(`${logPrefix} [caretTrace] [selection] rep.selStart after ace_performSelectionChange: Line=${repAfterSelectionChange.selStart[0]}, Col=${repAfterSelectionChange.selStart[1]}`);
1857
- // log(`${logPrefix} -> Selection set immediately.`);
1851
+ // log(`${logPrefix} [caretTrace] [selection] rep.selStart after ace_performSelectionChange: Line=${repAfterSelectionChange.selStart[0]}, Col=${repAfterSelectionChange.selStart[1]}`);
1852
+ // log(`${logPrefix} -> Selection set immediately.`);
1858
1853
 
1859
1854
  // Add sync hint AFTER setting selection
1860
1855
  editorInfo.ace_fastIncorp(1);
1861
1856
  const repAfterFastIncorp = editorInfo.ace_getRep();
1862
- // log(`${logPrefix} [caretTrace] [selection] rep.selStart after ace_fastIncorp: Line=${repAfterFastIncorp.selStart[0]}, Col=${repAfterFastIncorp.selStart[1]}`);
1863
- // log(`${logPrefix} -> Requested sync hint (fastIncorp 1).`);
1857
+ // log(`${logPrefix} [caretTrace] [selection] rep.selStart after ace_fastIncorp: Line=${repAfterFastIncorp.selStart[0]}, Col=${repAfterFastIncorp.selStart[1]}`);
1858
+ // log(`${logPrefix} -> Requested sync hint (fastIncorp 1).`);
1864
1859
 
1865
1860
  // --- Re-assert selection ---
1866
1861
  const targetCaretPosForReassert = [currentLineNum, newAbsoluteCaretCol];
1867
- // log(`${logPrefix} [caretTrace] Attempting to re-assert selection post-fastIncorp to [${targetCaretPosForReassert[0]}, ${targetCaretPosForReassert[1]}]`);
1862
+ // log(`${logPrefix} [caretTrace] Attempting to re-assert selection post-fastIncorp to [${targetCaretPosForReassert[0]}, ${targetCaretPosForReassert[1]}]`);
1868
1863
  editorInfo.ace_performSelectionChange(targetCaretPosForReassert, targetCaretPosForReassert, false);
1869
1864
  const repAfterReassert = editorInfo.ace_getRep();
1870
- // log(`${logPrefix} [caretTrace] [selection] rep.selStart after re-asserting selection: Line=${repAfterReassert.selStart[0]}, Col=${repAfterReassert.selStart[1]}`);
1865
+ // log(`${logPrefix} [caretTrace] [selection] rep.selStart after re-asserting selection: Line=${repAfterReassert.selStart[0]}, Col=${repAfterReassert.selStart[1]}`);
1871
1866
 
1872
1867
  // Store the updated caret info for the next event
1873
1868
  const newRelativePos = newAbsoluteCaretCol - cellStartCol;
@@ -1877,19 +1872,19 @@ exports.aceKeyEvent = (h, ctx) => {
1877
1872
  cellIndex: targetCellIndex,
1878
1873
  relativePos: newRelativePos
1879
1874
  };
1880
- // log(`${logPrefix} -> Updated stored click/caret info:`, editor.ep_data_tables_last_clicked);
1881
- // log(`${logPrefix} [caretTrace] Updated ep_data_tables_last_clicked. Line=${editor.ep_data_tables_last_clicked.lineNum}, Cell=${editor.ep_data_tables_last_clicked.cellIndex}, RelPos=${editor.ep_data_tables_last_clicked.relativePos}`);
1875
+ // log(`${logPrefix} -> Updated stored click/caret info:`, editor.ep_data_tables_last_clicked);
1876
+ // log(`${logPrefix} [caretTrace] Updated ep_data_tables_last_clicked. Line=${editor.ep_data_tables_last_clicked.lineNum}, Cell=${editor.ep_data_tables_last_clicked.cellIndex}, RelPos=${editor.ep_data_tables_last_clicked.relativePos}`);
1882
1877
 
1883
1878
 
1884
1879
  } catch (selError) {
1885
1880
  console.error(`${logPrefix} -> ERROR setting selection immediately:`, selError);
1886
1881
  }
1887
1882
  } else {
1888
- // log(`${logPrefix} -> Warning: newAbsoluteCaretCol not set, skipping selection update.`);
1883
+ // log(`${logPrefix} -> Warning: newAbsoluteCaretCol not set, skipping selection update.`);
1889
1884
  }
1890
1885
 
1891
1886
  } catch (error) {
1892
- // log(`${logPrefix} ERROR during manual key handling:`, error);
1887
+ // log(`${logPrefix} ERROR during manual key handling:`, error);
1893
1888
  console.error('[ep_data_tables] Error processing key event update:', error);
1894
1889
  // Maybe return false to allow default as a fallback on error?
1895
1890
  // For now, return true as we prevented default.
@@ -1897,7 +1892,7 @@ exports.aceKeyEvent = (h, ctx) => {
1897
1892
  }
1898
1893
 
1899
1894
  const endLogTime = Date.now();
1900
- // log(`${logPrefix} END (Handled Internal Edit Manually) Key='${evt.key}' Type='${evt.type}' -> Returned true. Duration: ${endLogTime - startLogTime}ms`);
1895
+ // log(`${logPrefix} END (Handled Internal Edit Manually) Key='${evt.key}' Type='${evt.type}' -> Returned true. Duration: ${endLogTime - startLogTime}ms`);
1901
1896
  return true; // We handled the key event
1902
1897
 
1903
1898
  } // End if(isTypingKey || isInternalBackspace || isInternalDelete)
@@ -1905,90 +1900,204 @@ exports.aceKeyEvent = (h, ctx) => {
1905
1900
 
1906
1901
  // Fallback for any other keys or edge cases not handled above
1907
1902
  const endLogTimeFinal = Date.now();
1908
- // log(`${logPrefix} END (Fell Through / Unhandled Case) Key='${evt.key}' Type='${evt.type}'. Allowing default. Duration: ${endLogTimeFinal - startLogTime}ms`);
1903
+ // log(`${logPrefix} END (Fell Through / Unhandled Case) Key='${evt.key}' Type='${evt.type}'. Allowing default. Duration: ${endLogTimeFinal - startLogTime}ms`);
1909
1904
  // Clear click state if it wasn't handled?
1910
1905
  // if (editor?.ep_data_tables_last_clicked) editor.ep_data_tables_last_clicked = null;
1911
- // log(`${logPrefix} [caretTrace] Final rep.selStart at end of aceKeyEvent (if unhandled): Line=${rep.selStart[0]}, Col=${rep.selStart[1]}`);
1906
+ // log(`${logPrefix} [caretTrace] Final rep.selStart at end of aceKeyEvent (if unhandled): Line=${rep.selStart[0]}, Col=${rep.selStart[1]}`);
1912
1907
  return false; // Allow default browser/ACE handling
1913
1908
  };
1914
-
1915
1909
  // ───────────────────── ace init + public helpers ─────────────────────
1916
1910
  exports.aceInitialized = (h, ctx) => {
1917
1911
  const logPrefix = '[ep_data_tables:aceInitialized]';
1918
- // log(`${logPrefix} START`, { hook_name: h, context: ctx });
1912
+ // log(`${logPrefix} START`, { hook_name: h, context: ctx });
1919
1913
  const ed = ctx.editorInfo;
1920
1914
  const docManager = ctx.documentAttributeManager;
1921
1915
 
1922
- // log(`${logPrefix} Attaching ep_data_tables_applyMeta helper to editorInfo.`);
1916
+ // log(`${logPrefix} Attaching ep_data_tables_applyMeta helper to editorInfo.`);
1923
1917
  ed.ep_data_tables_applyMeta = applyTableLineMetadataAttribute;
1924
- // log(`${logPrefix}: Attached applyTableLineMetadataAttribute helper to ed.ep_data_tables_applyMeta successfully.`);
1918
+ // log(`${logPrefix}: Attached applyTableLineMetadataAttribute helper to ed.ep_data_tables_applyMeta successfully.`);
1925
1919
 
1926
1920
  // Store the documentAttributeManager reference for later use
1927
- // log(`${logPrefix} Storing documentAttributeManager reference on editorInfo.`);
1921
+ // log(`${logPrefix} Storing documentAttributeManager reference on editorInfo.`);
1928
1922
  ed.ep_data_tables_docManager = docManager;
1929
- // log(`${logPrefix}: Stored documentAttributeManager reference as ed.ep_data_tables_docManager.`);
1923
+ // log(`${logPrefix}: Stored documentAttributeManager reference as ed.ep_data_tables_docManager.`);
1930
1924
 
1931
1925
  // *** ENHANCED: Paste event listener + Column resize listeners ***
1932
- // log(`${logPrefix} Preparing to attach paste and resize listeners via ace_callWithAce.`);
1926
+ // log(`${logPrefix} Preparing to attach paste and resize listeners via ace_callWithAce.`);
1933
1927
  ed.ace_callWithAce((ace) => {
1934
1928
  const callWithAceLogPrefix = '[ep_data_tables:aceInitialized:callWithAceForListeners]';
1935
- // log(`${callWithAceLogPrefix} Entered ace_callWithAce callback for listeners.`);
1929
+ // log(`${callWithAceLogPrefix} Entered ace_callWithAce callback for listeners.`);
1936
1930
 
1937
1931
  if (!ace || !ace.editor) {
1938
1932
  console.error(`${callWithAceLogPrefix} ERROR: ace or ace.editor is not available. Cannot attach listeners.`);
1939
- // log(`${callWithAceLogPrefix} Aborting listener attachment due to missing ace.editor.`);
1933
+ // log(`${callWithAceLogPrefix} Aborting listener attachment due to missing ace.editor.`);
1940
1934
  return;
1941
1935
  }
1942
1936
  const editor = ace.editor;
1943
- // log(`${callWithAceLogPrefix} ace.editor obtained successfully.`);
1937
+ // log(`${callWithAceLogPrefix} ace.editor obtained successfully.`);
1944
1938
 
1945
1939
  // Store editor reference for later use in table operations
1946
- // log(`${logPrefix} Storing editor reference on editorInfo.`);
1940
+ // log(`${logPrefix} Storing editor reference on editorInfo.`);
1947
1941
  ed.ep_data_tables_editor = editor;
1948
- // log(`${logPrefix}: Stored editor reference as ed.ep_data_tables_editor.`);
1942
+ // log(`${logPrefix}: Stored editor reference as ed.ep_data_tables_editor.`);
1949
1943
 
1950
1944
  // Attempt to find the inner iframe body, similar to ep_image_insert
1951
1945
  let $inner;
1952
1946
  try {
1953
- // log(`${callWithAceLogPrefix} Attempting to find inner iframe body for listener attachment.`);
1947
+ // log(`${callWithAceLogPrefix} Attempting to find inner iframe body for listener attachment.`);
1954
1948
  const $iframeOuter = $('iframe[name="ace_outer"]');
1955
1949
  if ($iframeOuter.length === 0) {
1956
1950
  console.error(`${callWithAceLogPrefix} ERROR: Could not find outer iframe (ace_outer).`);
1957
- // log(`${callWithAceLogPrefix} Failed to find ace_outer.`);
1951
+ // log(`${callWithAceLogPrefix} Failed to find ace_outer.`);
1958
1952
  return;
1959
1953
  }
1960
- // log(`${callWithAceLogPrefix} Found ace_outer:`, $iframeOuter);
1954
+ // log(`${callWithAceLogPrefix} Found ace_outer:`, $iframeOuter);
1961
1955
 
1962
1956
  const $iframeInner = $iframeOuter.contents().find('iframe[name="ace_inner"]');
1963
1957
  if ($iframeInner.length === 0) {
1964
1958
  console.error(`${callWithAceLogPrefix} ERROR: Could not find inner iframe (ace_inner).`);
1965
- // log(`${callWithAceLogPrefix} Failed to find ace_inner within ace_outer.`);
1959
+ // log(`${callWithAceLogPrefix} Failed to find ace_inner within ace_outer.`);
1966
1960
  return;
1967
1961
  }
1968
- // log(`${callWithAceLogPrefix} Found ace_inner:`, $iframeInner);
1962
+ // log(`${callWithAceLogPrefix} Found ace_inner:`, $iframeInner);
1969
1963
 
1970
1964
  const innerDocBody = $iframeInner.contents().find('body');
1971
1965
  if (innerDocBody.length === 0) {
1972
1966
  console.error(`${callWithAceLogPrefix} ERROR: Could not find body element in inner iframe.`);
1973
- // log(`${callWithAceLogPrefix} Failed to find body in ace_inner.`);
1967
+ // log(`${callWithAceLogPrefix} Failed to find body in ace_inner.`);
1974
1968
  return;
1975
1969
  }
1976
1970
  $inner = $(innerDocBody[0]); // Ensure it's a jQuery object of the body itself
1977
- // log(`${callWithAceLogPrefix} Successfully found inner iframe body:`, $inner);
1971
+ // log(`${callWithAceLogPrefix} Successfully found inner iframe body:`, $inner);
1972
+
1973
+ // ──────────────────────────────────────────────────────────────────────────────
1974
+ // Mobile suggestion / autocorrect guard (Android and iOS)
1975
+ // Many virtual keyboards commit the chosen suggestion AFTER the composition
1976
+ // session has ended. This arrives as a second `beforeinput` (or `input` on
1977
+ // Safari) with inputType = "insertReplacementText" or "insertFromComposition"
1978
+ // – sometimes just plain "insertText" with isComposing=false. The default DOM
1979
+ // mutation breaks our \u241F-delimited table lines, causing renderer failure.
1980
+ //
1981
+ // We intercept these in the capture phase, cancel the event, and inject a
1982
+ // single safe space via Ace APIs so the caret advances but the table
1983
+ // structure remains intact.
1984
+ // ──────────────────────────────────────────────────────────────────────────────
1985
+ const mobileSuggestionBlocker = (evt) => {
1986
+ const t = evt && evt.inputType || '';
1987
+ const dataStr = (evt && typeof evt.data === 'string') ? evt.data : '';
1988
+ const isProblem = (
1989
+ t === 'insertReplacementText' ||
1990
+ t === 'insertFromComposition' ||
1991
+ (t === 'insertText' && !evt.isComposing && (!dataStr || dataStr.length > 1))
1992
+ );
1993
+ if (!isProblem) return;
1994
+
1995
+ // Only act if selection is inside a table line
1996
+ try {
1997
+ const repQuick = ed.ace_getRep && ed.ace_getRep();
1998
+ if (!repQuick || !repQuick.selStart) return; // don't block outside editor selection
1999
+ const lineNumQuick = repQuick.selStart[0];
2000
+ let metaStrQuick = docManager && docManager.getAttributeOnLine
2001
+ ? docManager.getAttributeOnLine(lineNumQuick, ATTR_TABLE_JSON)
2002
+ : null;
2003
+ let metaQuick = null;
2004
+ if (metaStrQuick) { try { metaQuick = JSON.parse(metaStrQuick); } catch (_) {} }
2005
+ if (!metaQuick) metaQuick = getTableLineMetadata(lineNumQuick, ed, docManager);
2006
+ if (!metaQuick || typeof metaQuick.cols !== 'number') return; // not a table line → do not block
2007
+ } catch (_) { return; }
2008
+
2009
+ // Cancel the browser's DOM mutation early
2010
+ evt.preventDefault();
2011
+ if (typeof evt.stopImmediatePropagation === 'function') evt.stopImmediatePropagation();
2012
+
2013
+ // Replace selection with a single plain space using Ace APIs
2014
+ setTimeout(() => {
2015
+ try {
2016
+ ed.ace_callWithAce((aceInstance) => {
2017
+ aceInstance.ace_fastIncorp(10);
2018
+ const rep = aceInstance.ace_getRep();
2019
+ if (!rep || !rep.selStart) return;
2020
+ const lineNum = rep.selStart[0];
2021
+
2022
+ // Perform the safe insertion
2023
+ aceInstance.ace_performDocumentReplaceRange(rep.selStart, rep.selEnd, ' ');
2024
+
2025
+ // Try to re-apply table metadata attribute to stabilize renderer
2026
+ try {
2027
+ let metaStr = docManager && docManager.getAttributeOnLine
2028
+ ? docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON)
2029
+ : null;
2030
+ let meta = null;
2031
+ if (metaStr) { try { meta = JSON.parse(metaStr); } catch (_) {} }
2032
+ if (!meta) meta = getTableLineMetadata(lineNum, ed, docManager);
2033
+ if (meta && typeof meta.tblId !== 'undefined' && typeof meta.row !== 'undefined' && typeof meta.cols === 'number') {
2034
+ const repAfter = aceInstance.ace_getRep();
2035
+ ed.ep_data_tables_applyMeta(lineNum, meta.tblId, meta.row, meta.cols, repAfter, ed, null, docManager);
2036
+ }
2037
+ } catch (_) {}
2038
+ }, 'mobileSuggestionBlocker', true);
2039
+ } catch (e) {
2040
+ console.error('[ep_data_tables:mobileSuggestionBlocker] Error inserting space:', e);
2041
+ }
2042
+ }, 0);
2043
+ };
2044
+
2045
+ // Listen in capture phase so we win the race with browser/default handlers
2046
+ if ($inner && $inner.length > 0 && $inner[0].addEventListener) {
2047
+ $inner[0].addEventListener('beforeinput', mobileSuggestionBlocker, true);
2048
+ }
2049
+
2050
+ // Fallback for Safari iOS which may not emit beforeinput reliably: use input
2051
+ if ($inner && $inner.length > 0 && $inner[0].addEventListener) {
2052
+ $inner[0].addEventListener('input', (evt) => {
2053
+ // Only run if we did NOT already block in beforeinput
2054
+ if (evt && evt.inputType === 'insertText' && typeof evt.data === 'string' && evt.data.length > 1) {
2055
+ mobileSuggestionBlocker(evt);
2056
+ }
2057
+ }, true);
2058
+ }
2059
+
2060
+ // Android legacy fallback: some keyboards dispatch 'textInput' instead of beforeinput
2061
+ if (isAndroidUA && isAndroidUA() && $inner && $inner.length > 0 && $inner[0].addEventListener) {
2062
+ $inner[0].addEventListener('textInput', (evt) => {
2063
+ const s = typeof evt.data === 'string' ? evt.data : '';
2064
+ if (s && s.length > 1) {
2065
+ mobileSuggestionBlocker({
2066
+ inputType: 'insertText',
2067
+ isComposing: false,
2068
+ data: s,
2069
+ preventDefault: () => { try { evt.preventDefault(); } catch (_) {} },
2070
+ stopImmediatePropagation: () => { try { evt.stopImmediatePropagation(); } catch (_) {} },
2071
+ });
2072
+ }
2073
+ }, true);
2074
+ }
2075
+
2076
+ // Reduce chance of autocorrect/spellcheck kicking in at all
2077
+ try {
2078
+ const disableAuto = (el) => {
2079
+ if (!el) return;
2080
+ el.setAttribute('autocorrect', 'off');
2081
+ el.setAttribute('autocomplete', 'off');
2082
+ el.setAttribute('autocapitalize', 'off');
2083
+ el.setAttribute('spellcheck', 'false');
2084
+ };
2085
+ disableAuto(innerDocBody[0] || innerDocBody);
2086
+ } catch (_) {}
1978
2087
  } catch (e) {
1979
2088
  console.error(`${callWithAceLogPrefix} ERROR: Exception while trying to find inner iframe body:`, e);
1980
- // log(`${callWithAceLogPrefix} Exception details:`, { message: e.message, stack: e.stack });
2089
+ // log(`${callWithAceLogPrefix} Exception details:`, { message: e.message, stack: e.stack });
1981
2090
  return;
1982
2091
  }
1983
2092
 
1984
2093
  if (!$inner || $inner.length === 0) {
1985
2094
  console.error(`${callWithAceLogPrefix} ERROR: $inner is not valid after attempting to find iframe body. Cannot attach listeners.`);
1986
- // log(`${callWithAceLogPrefix} $inner is invalid. Aborting.`);
2095
+ // log(`${callWithAceLogPrefix} $inner is invalid. Aborting.`);
1987
2096
  return;
1988
2097
  }
1989
2098
 
1990
2099
  // *** CUT EVENT LISTENER ***
1991
- log(`${callWithAceLogPrefix} Attaching cut event listener to $inner (inner iframe body).`);
2100
+ // log(`${callWithAceLogPrefix} Attaching cut event listener to $inner (inner iframe body).`);
1992
2101
  $inner.on('cut', (evt) => {
1993
2102
  const cutLogPrefix = '[ep_data_tables:cutHandler]';
1994
2103
  console.log(`${cutLogPrefix} CUT EVENT TRIGGERED. Event object:`, evt);
@@ -2194,32 +2303,32 @@ exports.aceInitialized = (h, ctx) => {
2194
2303
  });
2195
2304
 
2196
2305
  // *** BEFOREINPUT EVENT LISTENER FOR CONTEXT-MENU DELETE ***
2197
- // log(`${callWithAceLogPrefix} Attaching beforeinput event listener to $inner (inner iframe body).`);
2306
+ // log(`${callWithAceLogPrefix} Attaching beforeinput event listener to $inner (inner iframe body).`);
2198
2307
  $inner.on('beforeinput', (evt) => {
2199
2308
  const deleteLogPrefix = '[ep_data_tables:beforeinputDeleteHandler]';
2200
- // log(`${deleteLogPrefix} BEFOREINPUT EVENT TRIGGERED. inputType: "${evt.originalEvent.inputType}", event object:`, evt);
2309
+ // log(`${deleteLogPrefix} BEFOREINPUT EVENT TRIGGERED. inputType: "${evt.originalEvent.inputType}", event object:`, evt);
2201
2310
 
2202
2311
  // Only intercept deletion-related input events
2203
2312
  if (!evt.originalEvent.inputType || !evt.originalEvent.inputType.startsWith('delete')) {
2204
- // log(`${deleteLogPrefix} Not a deletion event (inputType: "${evt.originalEvent.inputType}"). Allowing default.`);
2313
+ // log(`${deleteLogPrefix} Not a deletion event (inputType: "${evt.originalEvent.inputType}"). Allowing default.`);
2205
2314
  return; // Allow default for non-delete events
2206
2315
  }
2207
2316
 
2208
- // log(`${deleteLogPrefix} Getting current editor representation (rep).`);
2317
+ // log(`${deleteLogPrefix} Getting current editor representation (rep).`);
2209
2318
  const rep = ed.ace_getRep();
2210
2319
  if (!rep || !rep.selStart) {
2211
- // log(`${deleteLogPrefix} WARNING: Could not get representation or selection. Allowing default delete.`);
2320
+ // log(`${deleteLogPrefix} WARNING: Could not get representation or selection. Allowing default delete.`);
2212
2321
  console.warn(`${deleteLogPrefix} Could not get rep or selStart.`);
2213
2322
  return; // Allow default
2214
2323
  }
2215
- // log(`${deleteLogPrefix} Rep obtained. selStart:`, rep.selStart, `selEnd:`, rep.selEnd);
2324
+ // log(`${deleteLogPrefix} Rep obtained. selStart:`, rep.selStart, `selEnd:`, rep.selEnd);
2216
2325
  const selStart = rep.selStart;
2217
2326
  const selEnd = rep.selEnd;
2218
2327
  const lineNum = selStart[0];
2219
- // log(`${deleteLogPrefix} Current line number: ${lineNum}. Column start: ${selStart[1]}, Column end: ${selEnd[1]}.`);
2328
+ // log(`${deleteLogPrefix} Current line number: ${lineNum}. Column start: ${selStart[1]}, Column end: ${selEnd[1]}.`);
2220
2329
 
2221
2330
  // Android Chrome IME: collapsed backspace/forward-delete often comes via beforeinput
2222
- const isAndroidChrome = isAndroidChromiumUA();
2331
+ const isAndroidChrome = isAndroidUA();
2223
2332
  const inputType = (evt.originalEvent && evt.originalEvent.inputType) || '';
2224
2333
 
2225
2334
  // Handle collapsed deletes on Android Chrome inside a table line to protect delimiters
@@ -2315,12 +2424,12 @@ exports.aceInitialized = (h, ctx) => {
2315
2424
 
2316
2425
  // Check if selection spans multiple lines
2317
2426
  if (selStart[0] !== selEnd[0]) {
2318
- // log(`${deleteLogPrefix} WARNING: Selection spans multiple lines. Preventing delete to protect table structure.`);
2427
+ // log(`${deleteLogPrefix} WARNING: Selection spans multiple lines. Preventing delete to protect table structure.`);
2319
2428
  evt.preventDefault();
2320
2429
  return;
2321
2430
  }
2322
2431
 
2323
- // log(`${deleteLogPrefix} Checking if line ${lineNum} is a table line by fetching '${ATTR_TABLE_JSON}' attribute.`);
2432
+ // log(`${deleteLogPrefix} Checking if line ${lineNum} is a table line by fetching '${ATTR_TABLE_JSON}' attribute.`);
2324
2433
  let lineAttrString = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
2325
2434
  let tableMetadata = null;
2326
2435
 
@@ -2337,11 +2446,11 @@ exports.aceInitialized = (h, ctx) => {
2337
2446
  }
2338
2447
 
2339
2448
  if (!tableMetadata || typeof tableMetadata.cols !== 'number' || typeof tableMetadata.tblId === 'undefined' || typeof tableMetadata.row === 'undefined') {
2340
- // log(`${deleteLogPrefix} Line ${lineNum} is NOT a recognised table line. Allowing default delete.`);
2449
+ // log(`${deleteLogPrefix} Line ${lineNum} is NOT a recognised table line. Allowing default delete.`);
2341
2450
  return; // Not a table line
2342
2451
  }
2343
2452
 
2344
- // log(`${deleteLogPrefix} Line ${lineNum} IS a table line. Metadata:`, tableMetadata);
2453
+ // log(`${deleteLogPrefix} Line ${lineNum} IS a table line. Metadata:`, tableMetadata);
2345
2454
 
2346
2455
  // Validate selection is within cell boundaries
2347
2456
  const lineText = rep.lines.atIndex(lineNum)?.text || '';
@@ -2392,25 +2501,25 @@ exports.aceInitialized = (h, ctx) => {
2392
2501
  }
2393
2502
 
2394
2503
  if (targetCellIndex === -1 || selEnd[1] > cellEndCol) {
2395
- // log(`${deleteLogPrefix} WARNING: Selection spans cell boundaries or is outside cells. Preventing delete to protect table structure.`);
2504
+ // log(`${deleteLogPrefix} WARNING: Selection spans cell boundaries or is outside cells. Preventing delete to protect table structure.`);
2396
2505
  evt.preventDefault();
2397
2506
  return;
2398
2507
  }
2399
2508
 
2400
2509
  // If we reach here, the selection is entirely within a single cell - intercept delete and preserve table structure
2401
- // log(`${deleteLogPrefix} Selection is entirely within cell ${targetCellIndex}. Intercepting delete to preserve table structure.`);
2510
+ // log(`${deleteLogPrefix} Selection is entirely within cell ${targetCellIndex}. Intercepting delete to preserve table structure.`);
2402
2511
  evt.preventDefault();
2403
2512
 
2404
2513
  try {
2405
2514
  // No clipboard operations needed for delete - just perform the deletion within the cell using ace operations
2406
- // log(`${deleteLogPrefix} Performing deletion via ed.ace_callWithAce.`);
2515
+ // log(`${deleteLogPrefix} Performing deletion via ed.ace_callWithAce.`);
2407
2516
  ed.ace_callWithAce((aceInstance) => {
2408
2517
  const callAceLogPrefix = `${deleteLogPrefix}[ace_callWithAceOps]`;
2409
- // log(`${callAceLogPrefix} Entered ace_callWithAce for delete operations. selStart:`, selStart, `selEnd:`, selEnd);
2518
+ // log(`${callAceLogPrefix} Entered ace_callWithAce for delete operations. selStart:`, selStart, `selEnd:`, selEnd);
2410
2519
 
2411
- // log(`${callAceLogPrefix} Calling aceInstance.ace_performDocumentReplaceRange to delete selected text.`);
2520
+ // log(`${callAceLogPrefix} Calling aceInstance.ace_performDocumentReplaceRange to delete selected text.`);
2412
2521
  aceInstance.ace_performDocumentReplaceRange(selStart, selEnd, '');
2413
- // log(`${callAceLogPrefix} ace_performDocumentReplaceRange successful.`);
2522
+ // log(`${callAceLogPrefix} ace_performDocumentReplaceRange successful.`);
2414
2523
 
2415
2524
  // --- Ensure cell is not left empty (zero-length) ---
2416
2525
  const repAfterDeletion = aceInstance.ace_getRep();
@@ -2419,7 +2528,7 @@ exports.aceInitialized = (h, ctx) => {
2419
2528
  const cellTextAfterDeletion = cellsAfterDeletion[targetCellIndex] || '';
2420
2529
 
2421
2530
  if (cellTextAfterDeletion.length === 0) {
2422
- // log(`${callAceLogPrefix} Cell ${targetCellIndex} became empty after delete – inserting single space to preserve structure.`);
2531
+ // log(`${callAceLogPrefix} Cell ${targetCellIndex} became empty after delete – inserting single space to preserve structure.`);
2423
2532
  const insertPos = [lineNum, selStart[1]]; // Start of the now-empty cell
2424
2533
  aceInstance.ace_performDocumentReplaceRange(insertPos, insertPos, ' ');
2425
2534
 
@@ -2431,9 +2540,9 @@ exports.aceInitialized = (h, ctx) => {
2431
2540
  );
2432
2541
  }
2433
2542
 
2434
- // log(`${callAceLogPrefix} Preparing to re-apply tbljson attribute to line ${lineNum}.`);
2543
+ // log(`${callAceLogPrefix} Preparing to re-apply tbljson attribute to line ${lineNum}.`);
2435
2544
  const repAfterDelete = aceInstance.ace_getRep();
2436
- // log(`${callAceLogPrefix} Fetched rep after delete for applyMeta. Line ${lineNum} text now: "${repAfterDelete.lines.atIndex(lineNum).text}"`);
2545
+ // log(`${callAceLogPrefix} Fetched rep after delete for applyMeta. Line ${lineNum} text now: "${repAfterDelete.lines.atIndex(lineNum).text}"`);
2437
2546
 
2438
2547
  ed.ep_data_tables_applyMeta(
2439
2548
  lineNum,
@@ -2445,22 +2554,22 @@ exports.aceInitialized = (h, ctx) => {
2445
2554
  null,
2446
2555
  docManager
2447
2556
  );
2448
- // log(`${callAceLogPrefix} tbljson attribute re-applied successfully via ep_data_tables_applyMeta.`);
2557
+ // log(`${callAceLogPrefix} tbljson attribute re-applied successfully via ep_data_tables_applyMeta.`);
2449
2558
 
2450
2559
  // Determine new caret position – one char forward if we inserted a space
2451
2560
  const newCaretAbsoluteCol = (cellTextAfterDeletion.length === 0) ? selStart[1] + 1 : selStart[1];
2452
2561
  const newCaretPos = [lineNum, newCaretAbsoluteCol];
2453
- // log(`${callAceLogPrefix} Setting caret position to: [${newCaretPos}].`);
2562
+ // log(`${callAceLogPrefix} Setting caret position to: [${newCaretPos}].`);
2454
2563
  aceInstance.ace_performSelectionChange(newCaretPos, newCaretPos, false);
2455
- // log(`${callAceLogPrefix} Selection change successful.`);
2564
+ // log(`${callAceLogPrefix} Selection change successful.`);
2456
2565
 
2457
- // log(`${callAceLogPrefix} Delete operations within ace_callWithAce completed successfully.`);
2566
+ // log(`${callAceLogPrefix} Delete operations within ace_callWithAce completed successfully.`);
2458
2567
  }, 'tableDeleteTextOperations', true);
2459
2568
 
2460
- // log(`${deleteLogPrefix} Delete operation completed successfully.`);
2569
+ // log(`${deleteLogPrefix} Delete operation completed successfully.`);
2461
2570
  } catch (error) {
2462
2571
  console.error(`${deleteLogPrefix} ERROR during delete operation:`, error);
2463
- // log(`${deleteLogPrefix} Delete operation failed. Error details:`, { message: error.message, stack: error.stack });
2572
+ // log(`${deleteLogPrefix} Delete operation failed. Error details:`, { message: error.message, stack: error.stack });
2464
2573
  }
2465
2574
  });
2466
2575
 
@@ -2472,8 +2581,8 @@ exports.aceInitialized = (h, ctx) => {
2472
2581
  // Only intercept insert types
2473
2582
  if (!inputType || !inputType.startsWith('insert')) return;
2474
2583
 
2475
- // Target only Android Chromium-family browsers (exclude iOS and Firefox)
2476
- if (!isAndroidChromiumUA()) return;
2584
+ // Target only Android browsers (exclude iOS)
2585
+ if (!isAndroidUA()) return;
2477
2586
 
2478
2587
  // Get current selection and ensure we are inside a table line
2479
2588
  const rep = ed.ace_getRep();
@@ -2512,17 +2621,60 @@ exports.aceInitialized = (h, ctx) => {
2512
2621
  currentOffset += cellLength + DELIMITER.length;
2513
2622
  }
2514
2623
 
2515
- // Clamp selection end if it includes a trailing delimiter of the same cell
2516
- if (targetCellIndex !== -1 && selEnd[1] === cellEndCol + DELIMITER.length) {
2517
- selEnd[1] = cellEndCol;
2518
- }
2519
-
2520
2624
  // If selection spills outside the cell boundaries, block to protect structure
2521
2625
  if (targetCellIndex === -1 || selEnd[1] > cellEndCol) {
2522
2626
  evt.preventDefault();
2523
2627
  return;
2524
2628
  }
2525
2629
 
2630
+ // iOS soft break (insertParagraph/insertLineBreak) → replace with plain space via Ace
2631
+ if (inputType === 'insertParagraph' || inputType === 'insertLineBreak') {
2632
+ evt.preventDefault();
2633
+ evt.stopPropagation();
2634
+ if (typeof evt.stopImmediatePropagation === 'function') evt.stopImmediatePropagation();
2635
+ setTimeout(() => {
2636
+ try {
2637
+ ed.ace_callWithAce((aceInstance) => {
2638
+ aceInstance.ace_fastIncorp(10);
2639
+ const freshRep = aceInstance.ace_getRep();
2640
+ const freshSelStart = freshRep.selStart;
2641
+ const freshSelEnd = freshRep.selEnd;
2642
+ // Replace soft break with space
2643
+ aceInstance.ace_performDocumentReplaceRange(freshSelStart, freshSelEnd, ' ');
2644
+
2645
+ // Apply td attr to inserted space
2646
+ const afterRep = aceInstance.ace_getRep();
2647
+ const maxLen = Math.max(0, afterRep.lines.atIndex(lineNum)?.text?.length || 0);
2648
+ const startCol = Math.min(Math.max(freshSelStart[1], 0), maxLen);
2649
+ const endCol = Math.min(startCol + 1, maxLen);
2650
+ if (endCol > startCol) {
2651
+ aceInstance.ace_performDocumentApplyAttributesToRange(
2652
+ [lineNum, startCol], [lineNum, endCol], [[ATTR_CELL, String(targetCellIndex)]]
2653
+ );
2654
+ }
2655
+
2656
+ // Re-apply line metadata
2657
+ ed.ep_data_tables_applyMeta(
2658
+ lineNum, tableMetadata.tblId, tableMetadata.row, tableMetadata.cols,
2659
+ afterRep, ed, null, docManager
2660
+ );
2661
+
2662
+ const newCaretPos = [lineNum, endCol];
2663
+ aceInstance.ace_performSelectionChange(newCaretPos, newCaretPos, false);
2664
+ aceInstance.ace_fastIncorp(10);
2665
+ }, 'iosSoftBreakToSpace', true);
2666
+ } catch (e) {
2667
+ console.error(`${autoLogPrefix} ERROR replacing soft break:`, e);
2668
+ }
2669
+ }, 0);
2670
+ return;
2671
+ }
2672
+
2673
+ // Clamp selection end if it includes a trailing delimiter of the same cell
2674
+ if (targetCellIndex !== -1 && selEnd[1] === cellEndCol + DELIMITER.length) {
2675
+ selEnd[1] = cellEndCol;
2676
+ }
2677
+
2526
2678
  // Handle line breaks by routing to Enter behavior
2527
2679
  if (inputType === 'insertParagraph' || inputType === 'insertLineBreak') {
2528
2680
  evt.preventDefault();
@@ -2636,9 +2788,300 @@ exports.aceInitialized = (h, ctx) => {
2636
2788
  }
2637
2789
  });
2638
2790
 
2791
+ // Desktop (non-Android/iOS) – sanitize NBSP and table delimiter on any insert* beforeinput
2792
+ $inner.on('beforeinput', (evt) => {
2793
+ const genericLogPrefix = '[ep_data_tables:beforeinputInsertTextGeneric]';
2794
+ const inputType = (evt.originalEvent && evt.originalEvent.inputType) || '';
2795
+ // log diagnostics for all insert* types on table lines
2796
+ if (!inputType || !inputType.startsWith('insert')) return;
2797
+
2798
+ // Skip on Android or iOS (they have dedicated handlers above)
2799
+ if (isAndroidUA() || isIOSUA()) return;
2800
+
2801
+ const rep = ed.ace_getRep();
2802
+ if (!rep || !rep.selStart) return;
2803
+ const selStart = rep.selStart;
2804
+ const selEnd = rep.selEnd;
2805
+ const lineNum = selStart[0];
2806
+
2807
+ // Ensure we are inside a table line by resolving metadata
2808
+ let lineAttrString = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
2809
+ let tableMetadata = null;
2810
+ if (lineAttrString) {
2811
+ try { tableMetadata = JSON.parse(lineAttrString); } catch (_) {}
2812
+ }
2813
+ if (!tableMetadata) tableMetadata = getTableLineMetadata(lineNum, ed, docManager);
2814
+ if (!tableMetadata || typeof tableMetadata.cols !== 'number') return; // not a table line
2815
+ console.debug(`${genericLogPrefix} event`, { inputType, data: evt.originalEvent && evt.originalEvent.data });
2816
+
2817
+ // Treat null data (composition/IME) as a single space to prevent NBSP/BR side effects
2818
+ const rawData = evt.originalEvent && typeof evt.originalEvent.data === 'string' ? evt.originalEvent.data : ' ';
2819
+
2820
+ // Replace NBSP and delimiter with plain space, remove zero-width chars
2821
+ const insertedText = rawData
2822
+ .replace(/\u00A0/g, ' ')
2823
+ .replace(new RegExp(DELIMITER, 'g'), ' ')
2824
+ .replace(/[\u200B\u200C\u200D\uFEFF]/g, '')
2825
+ .replace(/\s+/g, ' ');
2826
+
2827
+ if (!insertedText) { evt.preventDefault(); return; }
2828
+
2829
+ // Compute cell boundaries to keep selection within current cell
2830
+ const lineText = rep.lines.atIndex(lineNum)?.text || '';
2831
+ const cells = lineText.split(DELIMITER);
2832
+ let currentOffset = 0;
2833
+ let targetCellIndex = -1;
2834
+ let cellStartCol = 0;
2835
+ let cellEndCol = 0;
2836
+ for (let i = 0; i < cells.length; i++) {
2837
+ const len = cells[i]?.length ?? 0;
2838
+ const end = currentOffset + len;
2839
+ if (selStart[1] >= currentOffset && selStart[1] <= end) {
2840
+ targetCellIndex = i;
2841
+ cellStartCol = currentOffset;
2842
+ cellEndCol = end;
2843
+ break;
2844
+ }
2845
+ currentOffset += len + DELIMITER.length;
2846
+ }
2847
+ if (targetCellIndex === -1 || selEnd[1] > cellEndCol) { evt.preventDefault(); console.debug(`${genericLogPrefix} abort: selection outside cell`, { selStart, selEnd, cellStartCol, cellEndCol }); return; }
2848
+
2849
+ evt.preventDefault();
2850
+ evt.stopPropagation();
2851
+ if (typeof evt.stopImmediatePropagation === 'function') evt.stopImmediatePropagation();
2852
+
2853
+ try {
2854
+ ed.ace_callWithAce((ace) => {
2855
+ ace.ace_fastIncorp(10);
2856
+ const freshRep = ace.ace_getRep();
2857
+ const freshSelStart = freshRep.selStart;
2858
+ const freshSelEnd = freshRep.selEnd;
2859
+
2860
+ ace.ace_performDocumentReplaceRange(freshSelStart, freshSelEnd, insertedText);
2861
+
2862
+ // Re-apply td attribute on inserted range
2863
+ const afterRep = ace.ace_getRep();
2864
+ const lineEntry = afterRep.lines.atIndex(lineNum);
2865
+ const maxLen = lineEntry ? lineEntry.text.length : 0;
2866
+ const startCol = Math.min(Math.max(freshSelStart[1], 0), maxLen);
2867
+ const endCol = Math.min(startCol + insertedText.length, maxLen);
2868
+ if (endCol > startCol) {
2869
+ ace.ace_performDocumentApplyAttributesToRange([lineNum, startCol], [lineNum, endCol], [[ATTR_CELL, String(targetCellIndex)]]);
2870
+ }
2871
+
2872
+ // Re-apply tbljson line attribute
2873
+ ed.ep_data_tables_applyMeta(lineNum, tableMetadata.tblId, tableMetadata.row, tableMetadata.cols, afterRep, ed, null, docManager);
2874
+
2875
+ // Move caret to end of inserted text
2876
+ const newCaretPos = [lineNum, endCol];
2877
+ ace.ace_performSelectionChange(newCaretPos, newCaretPos, false);
2878
+ }, 'tableGenericInsertText', true);
2879
+ } catch (e) {
2880
+ console.error(`${genericLogPrefix} ERROR handling generic insertText:`, e);
2881
+ }
2882
+ });
2883
+
2884
+ // iOS (all browsers) – intercept autocorrect/IME commit replacements to protect table structure
2885
+ $inner.on('beforeinput', (evt) => {
2886
+ const autoLogPrefix = '[ep_data_tables:beforeinputAutoReplaceHandler]';
2887
+ const inputType = (evt.originalEvent && evt.originalEvent.inputType) || '';
2888
+
2889
+ // Only handle on iOS family UAs
2890
+ if (!isIOSUA()) return;
2891
+
2892
+ // Get current selection and ensure we are inside a table line (define before computing hasSelection)
2893
+ const rep = ed.ace_getRep();
2894
+ if (!rep || !rep.selStart) return;
2895
+ const selStart = rep.selStart;
2896
+ const selEnd = rep.selEnd;
2897
+ const lineNum = selStart[0];
2898
+
2899
+ // Intercept replacement/IME commit types; also catch iOS insertText when it behaves like autocorrect
2900
+ const dataStr = (evt.originalEvent && typeof evt.originalEvent.data === 'string') ? evt.originalEvent.data : '';
2901
+ const hasSelection = !(selStart[0] === selEnd[0] && selStart[1] === selEnd[1]);
2902
+ const looksLikeIOSAutoReplace = inputType === 'insertText' && dataStr.length > 1; // multi-char commit (often replacement)
2903
+ const insertTextNull = inputType === 'insertText' && dataStr === '' && !hasSelection; // predictive commit with null data (often NBSP)
2904
+ const shouldHandle = INPUTTYPE_REPLACEMENT_TYPES.has(inputType) || looksLikeIOSAutoReplace || (inputType === 'insertText' && (hasSelection || insertTextNull));
2905
+ if (!shouldHandle) return;
2906
+
2907
+ // Resolve table metadata for this line
2908
+ let lineAttrString = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
2909
+ let tableMetadata = null;
2910
+ if (lineAttrString) {
2911
+ try { tableMetadata = JSON.parse(lineAttrString); } catch (_) {}
2912
+ }
2913
+ if (!tableMetadata) tableMetadata = getTableLineMetadata(lineNum, ed, docManager);
2914
+ if (!tableMetadata || typeof tableMetadata.cols !== 'number' || typeof tableMetadata.tblId === 'undefined' || typeof tableMetadata.row === 'undefined') {
2915
+ return; // not a table line
2916
+ }
2917
+
2918
+ // Compute cell boundaries and target cell index
2919
+ const lineText = rep.lines.atIndex(lineNum)?.text || '';
2920
+ const cells = lineText.split(DELIMITER);
2921
+ let currentOffset = 0;
2922
+ let targetCellIndex = -1;
2923
+ let cellStartCol = 0;
2924
+ let cellEndCol = 0;
2925
+ for (let i = 0; i < cells.length; i++) {
2926
+ const cellLength = cells[i]?.length ?? 0;
2927
+ const cellEndColThisIteration = currentOffset + cellLength;
2928
+ if (selStart[1] >= currentOffset && selStart[1] <= cellEndColThisIteration) {
2929
+ targetCellIndex = i;
2930
+ cellStartCol = currentOffset;
2931
+ cellEndCol = cellEndColThisIteration;
2932
+ break;
2933
+ }
2934
+ currentOffset += cellLength + DELIMITER.length;
2935
+ }
2936
+
2937
+ // Clamp selection end if it includes a trailing delimiter of the same cell
2938
+ if (targetCellIndex !== -1 && selEnd[1] === cellEndCol + DELIMITER.length) {
2939
+ selEnd[1] = cellEndCol;
2940
+ }
2941
+
2942
+ // If selection spills outside the cell boundaries, block to protect structure
2943
+ if (targetCellIndex === -1 || selEnd[1] > cellEndCol) {
2944
+ evt.preventDefault();
2945
+ return;
2946
+ }
2947
+
2948
+ // Replacement text payload from beforeinput
2949
+ let insertedText = dataStr;
2950
+ if (!insertedText) {
2951
+ if (insertTextNull) {
2952
+ // Predictive text commit: Safari already mutated DOM (&nbsp;<br>). Block and repair via Ace.
2953
+ evt.preventDefault();
2954
+ evt.stopPropagation();
2955
+ if (typeof evt.stopImmediatePropagation === 'function') evt.stopImmediatePropagation();
2956
+
2957
+ setTimeout(() => {
2958
+ try {
2959
+ ed.ace_callWithAce((aceInstance) => {
2960
+ aceInstance.ace_fastIncorp(10);
2961
+ const freshRep = aceInstance.ace_getRep();
2962
+ const freshSelStart = freshRep.selStart;
2963
+ const freshSelEnd = freshRep.selEnd;
2964
+ // Insert a plain space
2965
+ aceInstance.ace_performDocumentReplaceRange(freshSelStart, freshSelEnd, ' ');
2966
+
2967
+ // Re-apply cell attribute to the single inserted char
2968
+ const afterRep = aceInstance.ace_getRep();
2969
+ const maxLen = Math.max(0, afterRep.lines.atIndex(lineNum)?.text?.length || 0);
2970
+ const startCol = Math.min(Math.max(freshSelStart[1], 0), maxLen);
2971
+ const endCol = Math.min(startCol + 1, maxLen);
2972
+ if (endCol > startCol) {
2973
+ aceInstance.ace_performDocumentApplyAttributesToRange(
2974
+ [lineNum, startCol], [lineNum, endCol], [[ATTR_CELL, String(targetCellIndex)]]
2975
+ );
2976
+ }
2977
+
2978
+ // Re-apply table metadata line attribute
2979
+ ed.ep_data_tables_applyMeta(
2980
+ lineNum, tableMetadata.tblId, tableMetadata.row, tableMetadata.cols,
2981
+ afterRep, ed, null, docManager
2982
+ );
2983
+
2984
+ // Move caret to end of inserted text
2985
+ const newCaretPos = [lineNum, endCol];
2986
+ aceInstance.ace_performSelectionChange(newCaretPos, newCaretPos, false);
2987
+ aceInstance.ace_fastIncorp(10);
2988
+ }, 'iosPredictiveCommit', true);
2989
+ } catch (e) {
2990
+ console.error(`${autoLogPrefix} ERROR fixing predictive commit:`, e);
2991
+ }
2992
+ }, 0);
2993
+ return;
2994
+ } else {
2995
+ // No payload and not special: if it's a replacement, block default to avoid DOM mutation
2996
+ if (INPUTTYPE_REPLACEMENT_TYPES.has(inputType) || hasSelection) {
2997
+ evt.preventDefault();
2998
+ evt.stopPropagation();
2999
+ if (typeof evt.stopImmediatePropagation === 'function') evt.stopImmediatePropagation();
3000
+ }
3001
+ return;
3002
+ }
3003
+ }
3004
+
3005
+ // Sanitize inserted text: remove our delimiter and zero-width characters; normalize whitespace
3006
+ insertedText = insertedText
3007
+ .replace(new RegExp(DELIMITER, 'g'), ' ')
3008
+ .replace(/[\u200B\u200C\u200D\uFEFF]/g, '')
3009
+ .replace(/\s+/g, ' ');
3010
+
3011
+ // Intercept the browser default and perform the edit via Ace APIs
3012
+ evt.preventDefault();
3013
+ evt.stopPropagation();
3014
+ if (typeof evt.stopImmediatePropagation === 'function') evt.stopImmediatePropagation();
3015
+
3016
+ try {
3017
+ setTimeout(() => {
3018
+ ed.ace_callWithAce((aceInstance) => {
3019
+ aceInstance.ace_fastIncorp(10);
3020
+ const freshRep = aceInstance.ace_getRep();
3021
+ const freshSelStart = freshRep.selStart;
3022
+ const freshSelEnd = freshRep.selEnd;
3023
+
3024
+ // Replace selection with sanitized text
3025
+ aceInstance.ace_performDocumentReplaceRange(freshSelStart, freshSelEnd, insertedText);
3026
+
3027
+ // Re-apply cell attribute to the newly inserted text (clamped to line length)
3028
+ const repAfterReplace = aceInstance.ace_getRep();
3029
+ const freshLineIndex = freshSelStart[0];
3030
+ const freshLineEntry = repAfterReplace.lines.atIndex(freshLineIndex);
3031
+ const maxLen = Math.max(0, (freshLineEntry && freshLineEntry.text) ? freshLineEntry.text.length : 0);
3032
+ const startCol = Math.min(Math.max(freshSelStart[1], 0), maxLen);
3033
+ const endColRaw = startCol + insertedText.length;
3034
+ const endCol = Math.min(endColRaw, maxLen);
3035
+ if (endCol > startCol) {
3036
+ aceInstance.ace_performDocumentApplyAttributesToRange(
3037
+ [freshLineIndex, startCol], [freshLineIndex, endCol], [[ATTR_CELL, String(targetCellIndex)]]
3038
+ );
3039
+ }
3040
+
3041
+ // Re-apply table metadata line attribute
3042
+ ed.ep_data_tables_applyMeta(
3043
+ freshLineIndex,
3044
+ tableMetadata.tblId,
3045
+ tableMetadata.row,
3046
+ tableMetadata.cols,
3047
+ repAfterReplace,
3048
+ ed,
3049
+ null,
3050
+ docManager
3051
+ );
3052
+
3053
+ // Move caret to end of inserted text and update last-click state
3054
+ const newCaretCol = endCol;
3055
+ const newCaretPos = [freshLineIndex, newCaretCol];
3056
+ aceInstance.ace_performSelectionChange(newCaretPos, newCaretPos, false);
3057
+ aceInstance.ace_fastIncorp(10);
3058
+
3059
+ if (editor && editor.ep_data_tables_last_clicked && editor.ep_data_tables_last_clicked.tblId === tableMetadata.tblId) {
3060
+ // Recompute cellStartCol for fresh line
3061
+ const freshLineText = (freshLineEntry && freshLineEntry.text) || '';
3062
+ const freshCells = freshLineText.split(DELIMITER);
3063
+ let freshOffset = 0;
3064
+ for (let i = 0; i < targetCellIndex; i++) {
3065
+ freshOffset += (freshCells[i]?.length ?? 0) + DELIMITER.length;
3066
+ }
3067
+ const newRelativePos = newCaretCol - freshOffset;
3068
+ editor.ep_data_tables_last_clicked = {
3069
+ lineNum: freshLineIndex,
3070
+ tblId: tableMetadata.tblId,
3071
+ cellIndex: targetCellIndex,
3072
+ relativePos: newRelativePos < 0 ? 0 : newRelativePos,
3073
+ };
3074
+ }
3075
+ }, 'tableAutoReplaceTextOperations', true);
3076
+ }, 0);
3077
+ } catch (error) {
3078
+ console.error(`${autoLogPrefix} ERROR during auto-replace handling:`, error);
3079
+ }
3080
+ });
3081
+
2639
3082
  // Composition start marker (Android Chrome/table only)
2640
3083
  $inner.on('compositionstart', (evt) => {
2641
- if (!isAndroidChromiumUA()) return;
3084
+ if (!isAndroidUA()) return;
2642
3085
  const rep = ed.ace_getRep();
2643
3086
  if (!rep || !rep.selStart) return;
2644
3087
  const lineNum = rep.selStart[0];
@@ -2650,12 +3093,11 @@ exports.aceInitialized = (h, ctx) => {
2650
3093
  handledCurrentComposition = false;
2651
3094
  suppressBeforeInputInsertTextDuringComposition = false;
2652
3095
  });
2653
-
2654
3096
  // Android Chrome composition handling for whitespace (space) to prevent DOM mutation breaking delimiters
2655
3097
  $inner.on('compositionupdate', (evt) => {
2656
3098
  const compLogPrefix = '[ep_data_tables:compositionHandler]';
2657
3099
 
2658
- if (!isAndroidChromiumUA()) return;
3100
+ if (!isAndroidUA()) return;
2659
3101
 
2660
3102
  const rep = ed.ace_getRep();
2661
3103
  if (!rep || !rep.selStart) return;
@@ -2778,12 +3220,12 @@ exports.aceInitialized = (h, ctx) => {
2778
3220
  });
2779
3221
 
2780
3222
  // *** DRAG AND DROP EVENT LISTENERS ***
2781
- // log(`${callWithAceLogPrefix} Attaching drag and drop event listeners to $inner (inner iframe body).`);
3223
+ // log(`${callWithAceLogPrefix} Attaching drag and drop event listeners to $inner (inner iframe body).`);
2782
3224
 
2783
3225
  // Prevent drops that could damage table structure
2784
3226
  $inner.on('drop', (evt) => {
2785
3227
  const dropLogPrefix = '[ep_data_tables:dropHandler]';
2786
- // log(`${dropLogPrefix} DROP EVENT TRIGGERED. Event object:`, evt);
3228
+ // log(`${dropLogPrefix} DROP EVENT TRIGGERED. Event object:`, evt);
2787
3229
 
2788
3230
  // Block drops directly targeted at a table element regardless of selection state
2789
3231
  const targetEl = evt.target;
@@ -2797,19 +3239,19 @@ exports.aceInitialized = (h, ctx) => {
2797
3239
  return;
2798
3240
  }
2799
3241
 
2800
- // log(`${dropLogPrefix} Getting current editor representation (rep).`);
3242
+ // log(`${dropLogPrefix} Getting current editor representation (rep).`);
2801
3243
  const rep = ed.ace_getRep();
2802
3244
  if (!rep || !rep.selStart) {
2803
- // log(`${dropLogPrefix} WARNING: Could not get representation or selection. Allowing default drop.`);
3245
+ // log(`${dropLogPrefix} WARNING: Could not get representation or selection. Allowing default drop.`);
2804
3246
  return; // Allow default
2805
3247
  }
2806
3248
 
2807
3249
  const selStart = rep.selStart;
2808
3250
  const lineNum = selStart[0];
2809
- // log(`${dropLogPrefix} Current line number: ${lineNum}.`);
3251
+ // log(`${dropLogPrefix} Current line number: ${lineNum}.`);
2810
3252
 
2811
3253
  // Check if we're dropping onto a table line
2812
- // log(`${dropLogPrefix} Checking if line ${lineNum} is a table line by fetching '${ATTR_TABLE_JSON}' attribute.`);
3254
+ // log(`${dropLogPrefix} Checking if line ${lineNum} is a table line by fetching '${ATTR_TABLE_JSON}' attribute.`);
2813
3255
  let lineAttrString = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
2814
3256
  let isTableLine = !!lineAttrString;
2815
3257
 
@@ -2819,7 +3261,7 @@ exports.aceInitialized = (h, ctx) => {
2819
3261
  }
2820
3262
 
2821
3263
  if (isTableLine) {
2822
- // log(`${dropLogPrefix} Line ${lineNum} IS a table line. Preventing drop to protect table structure.`);
3264
+ // log(`${dropLogPrefix} Line ${lineNum} IS a table line. Preventing drop to protect table structure.`);
2823
3265
  evt.preventDefault();
2824
3266
  evt.stopPropagation();
2825
3267
  console.warn('[ep_data_tables] Drop operation prevented to protect table structure. Please use copy/paste within table cells.');
@@ -2850,7 +3292,7 @@ exports.aceInitialized = (h, ctx) => {
2850
3292
  const lineNum = selStart[0];
2851
3293
 
2852
3294
  // Check if we're dragging over a table line
2853
- // log(`${dragLogPrefix} Checking if line ${lineNum} is a table line by fetching '${ATTR_TABLE_JSON}' attribute.`);
3295
+ // log(`${dragLogPrefix} Checking if line ${lineNum} is a table line by fetching '${ATTR_TABLE_JSON}' attribute.`);
2854
3296
  let lineAttrString = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
2855
3297
  let isTableLine = !!lineAttrString;
2856
3298
 
@@ -2859,7 +3301,7 @@ exports.aceInitialized = (h, ctx) => {
2859
3301
  }
2860
3302
 
2861
3303
  if (isTableLine) {
2862
- // log(`${dragLogPrefix} Preventing dragover on table line ${lineNum} to control drop handling.`);
3304
+ // log(`${dragLogPrefix} Preventing dragover on table line ${lineNum} to control drop handling.`);
2863
3305
  evt.preventDefault();
2864
3306
  }
2865
3307
  });
@@ -2877,67 +3319,67 @@ exports.aceInitialized = (h, ctx) => {
2877
3319
  });
2878
3320
 
2879
3321
  // *** EXISTING PASTE LISTENER ***
2880
- // log(`${callWithAceLogPrefix} Attaching paste event listener to $inner (inner iframe body).`);
3322
+ // log(`${callWithAceLogPrefix} Attaching paste event listener to $inner (inner iframe body).`);
2881
3323
  $inner.on('paste', (evt) => {
2882
3324
  const pasteLogPrefix = '[ep_data_tables:pasteHandler]';
2883
- // log(`${pasteLogPrefix} PASTE EVENT TRIGGERED. Event object:`, evt);
3325
+ // log(`${pasteLogPrefix} PASTE EVENT TRIGGERED. Event object:`, evt);
2884
3326
 
2885
- // log(`${pasteLogPrefix} Getting current editor representation (rep).`);
3327
+ // log(`${pasteLogPrefix} Getting current editor representation (rep).`);
2886
3328
  const rep = ed.ace_getRep();
2887
3329
  if (!rep || !rep.selStart) {
2888
- // log(`${pasteLogPrefix} WARNING: Could not get representation or selection. Allowing default paste.`);
3330
+ // log(`${pasteLogPrefix} WARNING: Could not get representation or selection. Allowing default paste.`);
2889
3331
  console.warn(`${pasteLogPrefix} Could not get rep or selStart.`);
2890
3332
  return; // Allow default
2891
3333
  }
2892
- // log(`${pasteLogPrefix} Rep obtained. selStart:`, rep.selStart, `selEnd:`, rep.selEnd);
3334
+ // log(`${pasteLogPrefix} Rep obtained. selStart:`, rep.selStart, `selEnd:`, rep.selEnd);
2893
3335
  const selStart = rep.selStart;
2894
3336
  const selEnd = rep.selEnd;
2895
3337
  const lineNum = selStart[0];
2896
- // log(`${pasteLogPrefix} Current line number: ${lineNum}. Column start: ${selStart[1]}, Column end: ${selEnd[1]}.`);
3338
+ // log(`${pasteLogPrefix} Current line number: ${lineNum}. Column start: ${selStart[1]}, Column end: ${selEnd[1]}.`);
2897
3339
 
2898
3340
  // NEW: Check if selection spans multiple lines
2899
3341
  if (selStart[0] !== selEnd[0]) {
2900
- // log(`${pasteLogPrefix} WARNING: Selection spans multiple lines. Preventing paste to protect table structure.`);
3342
+ // log(`${pasteLogPrefix} WARNING: Selection spans multiple lines. Preventing paste to protect table structure.`);
2901
3343
  evt.preventDefault();
2902
3344
  return;
2903
3345
  }
2904
3346
 
2905
- // log(`${pasteLogPrefix} Checking if line ${lineNum} is a table line by fetching '${ATTR_TABLE_JSON}' attribute.`);
3347
+ // log(`${pasteLogPrefix} Checking if line ${lineNum} is a table line by fetching '${ATTR_TABLE_JSON}' attribute.`);
2906
3348
  let lineAttrString = docManager.getAttributeOnLine(lineNum, ATTR_TABLE_JSON);
2907
3349
  let tableMetadata = null;
2908
3350
 
2909
3351
  if (!lineAttrString) {
2910
3352
  // Block-styled row? Reconstruct metadata from the DOM.
2911
- // log(`${pasteLogPrefix} No '${ATTR_TABLE_JSON}' attribute found. Checking if this is a block-styled table row via DOM reconstruction.`);
3353
+ // log(`${pasteLogPrefix} No '${ATTR_TABLE_JSON}' attribute found. Checking if this is a block-styled table row via DOM reconstruction.`);
2912
3354
  const fallbackMeta = getTableLineMetadata(lineNum, ed, docManager);
2913
3355
  if (fallbackMeta) {
2914
3356
  tableMetadata = fallbackMeta;
2915
3357
  lineAttrString = JSON.stringify(fallbackMeta);
2916
- // log(`${pasteLogPrefix} Block-styled table row detected. Reconstructed metadata:`, fallbackMeta);
3358
+ // log(`${pasteLogPrefix} Block-styled table row detected. Reconstructed metadata:`, fallbackMeta);
2917
3359
  }
2918
3360
  }
2919
3361
 
2920
3362
  if (!lineAttrString) {
2921
- // log(`${pasteLogPrefix} Line ${lineNum} is NOT a table line (no '${ATTR_TABLE_JSON}' attribute found and no DOM reconstruction possible). Allowing default paste.`);
3363
+ // log(`${pasteLogPrefix} Line ${lineNum} is NOT a table line (no '${ATTR_TABLE_JSON}' attribute found and no DOM reconstruction possible). Allowing default paste.`);
2922
3364
  return; // Not a table line
2923
3365
  }
2924
- // log(`${pasteLogPrefix} Line ${lineNum} IS a table line. Attribute string: "${lineAttrString}".`);
3366
+ // log(`${pasteLogPrefix} Line ${lineNum} IS a table line. Attribute string: "${lineAttrString}".`);
2925
3367
 
2926
3368
  try {
2927
- // log(`${pasteLogPrefix} Parsing table metadata from attribute string.`);
3369
+ // log(`${pasteLogPrefix} Parsing table metadata from attribute string.`);
2928
3370
  if (!tableMetadata) {
2929
3371
  tableMetadata = JSON.parse(lineAttrString);
2930
3372
  }
2931
- // log(`${pasteLogPrefix} Parsed table metadata:`, tableMetadata);
3373
+ // log(`${pasteLogPrefix} Parsed table metadata:`, tableMetadata);
2932
3374
  if (!tableMetadata || typeof tableMetadata.cols !== 'number' || typeof tableMetadata.tblId === 'undefined' || typeof tableMetadata.row === 'undefined') {
2933
- // log(`${pasteLogPrefix} WARNING: Invalid or incomplete table metadata on line ${lineNum}. Allowing default paste. Metadata:`, tableMetadata);
3375
+ // log(`${pasteLogPrefix} WARNING: Invalid or incomplete table metadata on line ${lineNum}. Allowing default paste. Metadata:`, tableMetadata);
2934
3376
  console.warn(`${pasteLogPrefix} Invalid table metadata for line ${lineNum}.`);
2935
3377
  return; // Allow default
2936
3378
  }
2937
- // log(`${pasteLogPrefix} Table metadata validated successfully: tblId=${tableMetadata.tblId}, row=${tableMetadata.row}, cols=${tableMetadata.cols}.`);
3379
+ // log(`${pasteLogPrefix} Table metadata validated successfully: tblId=${tableMetadata.tblId}, row=${tableMetadata.row}, cols=${tableMetadata.cols}.`);
2938
3380
  } catch(e) {
2939
3381
  console.error(`${pasteLogPrefix} ERROR parsing table metadata for line ${lineNum}:`, e);
2940
- // log(`${pasteLogPrefix} Metadata parse error. Allowing default paste. Error details:`, { message: e.message, stack: e.stack });
3382
+ // log(`${pasteLogPrefix} Metadata parse error. Allowing default paste. Error details:`, { message: e.message, stack: e.stack });
2941
3383
  return; // Allow default
2942
3384
  }
2943
3385
 
@@ -2968,29 +3410,29 @@ exports.aceInitialized = (h, ctx) => {
2968
3410
  selEnd[1] = cellEndCol; // clamp
2969
3411
  }
2970
3412
  if (targetCellIndex === -1 || selEnd[1] > cellEndCol) {
2971
- // log(`${pasteLogPrefix} WARNING: Selection spans cell boundaries or is outside cells. Preventing paste to protect table structure.`);
3413
+ // log(`${pasteLogPrefix} WARNING: Selection spans cell boundaries or is outside cells. Preventing paste to protect table structure.`);
2972
3414
  evt.preventDefault();
2973
3415
  return;
2974
3416
  }
2975
3417
 
2976
- // log(`${pasteLogPrefix} Accessing clipboard data.`);
3418
+ // log(`${pasteLogPrefix} Accessing clipboard data.`);
2977
3419
  const clipboardData = evt.originalEvent.clipboardData || window.clipboardData;
2978
3420
  if (!clipboardData) {
2979
- // log(`${pasteLogPrefix} WARNING: No clipboard data found. Allowing default paste.`);
3421
+ // log(`${pasteLogPrefix} WARNING: No clipboard data found. Allowing default paste.`);
2980
3422
  return; // Allow default
2981
3423
  }
2982
- // log(`${pasteLogPrefix} Clipboard data object obtained:`, clipboardData);
3424
+ // log(`${pasteLogPrefix} Clipboard data object obtained:`, clipboardData);
2983
3425
 
2984
3426
  // Allow default handling (so ep_hyperlinked_text plugin can process) if rich HTML is present
2985
3427
  const types = clipboardData.types || [];
2986
3428
  if (types.includes('text/html') && clipboardData.getData('text/html')) {
2987
- // log(`${pasteLogPrefix} Detected text/html in clipboard – deferring to other plugins and default paste.`);
3429
+ // log(`${pasteLogPrefix} Detected text/html in clipboard – deferring to other plugins and default paste.`);
2988
3430
  return; // Do not intercept
2989
3431
  }
2990
3432
 
2991
- // log(`${pasteLogPrefix} Getting 'text/plain' from clipboard.`);
3433
+ // log(`${pasteLogPrefix} Getting 'text/plain' from clipboard.`);
2992
3434
  const pastedTextRaw = clipboardData.getData('text/plain');
2993
- // log(`${pasteLogPrefix} Pasted text raw: "${pastedTextRaw}" (Type: ${typeof pastedTextRaw})`);
3435
+ // log(`${pasteLogPrefix} Pasted text raw: "${pastedTextRaw}" (Type: ${typeof pastedTextRaw})`);
2994
3436
 
2995
3437
  // ENHANCED: More thorough sanitization of pasted content
2996
3438
  let pastedText = pastedTextRaw
@@ -3000,18 +3442,18 @@ exports.aceInitialized = (h, ctx) => {
3000
3442
  .replace(/\s+/g, " ") // Normalize whitespace
3001
3443
  .trim(); // Trim leading/trailing whitespace
3002
3444
 
3003
- // log(`${pasteLogPrefix} Pasted text after sanitization: "${pastedText}"`);
3445
+ // log(`${pasteLogPrefix} Pasted text after sanitization: "${pastedText}"`);
3004
3446
 
3005
3447
  if (typeof pastedText !== 'string' || pastedText.length === 0) {
3006
- // log(`${pasteLogPrefix} No plain text in clipboard or text is empty (after sanitization). Allowing default paste.`);
3448
+ // log(`${pasteLogPrefix} No plain text in clipboard or text is empty (after sanitization). Allowing default paste.`);
3007
3449
  const types = clipboardData.types;
3008
- // log(`${pasteLogPrefix} Clipboard types available:`, types);
3450
+ // log(`${pasteLogPrefix} Clipboard types available:`, types);
3009
3451
  if (types && types.includes('text/html')) {
3010
- // log(`${pasteLogPrefix} Clipboard also contains HTML:`, clipboardData.getData('text/html'));
3452
+ // log(`${pasteLogPrefix} Clipboard also contains HTML:`, clipboardData.getData('text/html'));
3011
3453
  }
3012
3454
  return; // Allow default if no plain text
3013
3455
  }
3014
- // log(`${pasteLogPrefix} Plain text obtained from clipboard: "${pastedText}". Length: ${pastedText.length}.`);
3456
+ // log(`${pasteLogPrefix} Plain text obtained from clipboard: "${pastedText}". Length: ${pastedText.length}.`);
3015
3457
 
3016
3458
  // NEW: Check if paste would exceed cell boundaries
3017
3459
  const currentCellText = cells[targetCellIndex] || '';
@@ -3025,18 +3467,18 @@ exports.aceInitialized = (h, ctx) => {
3025
3467
  // see fit or set to `Infinity` to remove the cap entirely.
3026
3468
  const MAX_CELL_LENGTH = 8000;
3027
3469
  if (newCellLength > MAX_CELL_LENGTH) {
3028
- // log(`${pasteLogPrefix} WARNING: Paste would exceed maximum cell length (${newCellLength} > ${MAX_CELL_LENGTH}). Truncating paste.`);
3470
+ // log(`${pasteLogPrefix} WARNING: Paste would exceed maximum cell length (${newCellLength} > ${MAX_CELL_LENGTH}). Truncating paste.`);
3029
3471
  const truncatedPaste = pastedText.substring(0, MAX_CELL_LENGTH - (currentCellText.length - selectionLength));
3030
3472
  if (truncatedPaste.length === 0) {
3031
- // log(`${pasteLogPrefix} Paste would be completely truncated. Preventing paste.`);
3473
+ // log(`${pasteLogPrefix} Paste would be completely truncated. Preventing paste.`);
3032
3474
  evt.preventDefault();
3033
3475
  return;
3034
3476
  }
3035
- // log(`${pasteLogPrefix} Using truncated paste: "${truncatedPaste}"`);
3477
+ // log(`${pasteLogPrefix} Using truncated paste: "${truncatedPaste}"`);
3036
3478
  pastedText = truncatedPaste;
3037
3479
  }
3038
3480
 
3039
- // log(`${pasteLogPrefix} INTERCEPTING paste of plain text into table line ${lineNum}. PREVENTING DEFAULT browser action.`);
3481
+ // log(`${pasteLogPrefix} INTERCEPTING paste of plain text into table line ${lineNum}. PREVENTING DEFAULT browser action.`);
3040
3482
  evt.preventDefault();
3041
3483
  // Prevent other plugins from handling the same paste event once we
3042
3484
  // have intercepted it inside a table cell.
@@ -3044,20 +3486,20 @@ exports.aceInitialized = (h, ctx) => {
3044
3486
  if (typeof evt.stopImmediatePropagation === 'function') evt.stopImmediatePropagation();
3045
3487
 
3046
3488
  try {
3047
- // log(`${pasteLogPrefix} Preparing to perform paste operations via ed.ace_callWithAce.`);
3489
+ // log(`${pasteLogPrefix} Preparing to perform paste operations via ed.ace_callWithAce.`);
3048
3490
  ed.ace_callWithAce((aceInstance) => {
3049
3491
  const callAceLogPrefix = `${pasteLogPrefix}[ace_callWithAceOps]`;
3050
- // log(`${callAceLogPrefix} Entered ace_callWithAce for paste operations. selStart:`, selStart, `selEnd:`, selEnd);
3492
+ // log(`${callAceLogPrefix} Entered ace_callWithAce for paste operations. selStart:`, selStart, `selEnd:`, selEnd);
3051
3493
 
3052
- // log(`${callAceLogPrefix} Original line text from initial rep: "${rep.lines.atIndex(lineNum).text}". SelStartCol: ${selStart[1]}, SelEndCol: ${selEnd[1]}.`);
3494
+ // log(`${callAceLogPrefix} Original line text from initial rep: "${rep.lines.atIndex(lineNum).text}". SelStartCol: ${selStart[1]}, SelEndCol: ${selEnd[1]}.`);
3053
3495
 
3054
- // log(`${callAceLogPrefix} Calling aceInstance.ace_performDocumentReplaceRange to insert text: "${pastedText}".`);
3496
+ // log(`${callAceLogPrefix} Calling aceInstance.ace_performDocumentReplaceRange to insert text: "${pastedText}".`);
3055
3497
  aceInstance.ace_performDocumentReplaceRange(selStart, selEnd, pastedText);
3056
- // log(`${callAceLogPrefix} ace_performDocumentReplaceRange successful.`);
3498
+ // log(`${callAceLogPrefix} ace_performDocumentReplaceRange successful.`);
3057
3499
 
3058
- // log(`${callAceLogPrefix} Preparing to re-apply tbljson attribute to line ${lineNum}.`);
3500
+ // log(`${callAceLogPrefix} Preparing to re-apply tbljson attribute to line ${lineNum}.`);
3059
3501
  const repAfterReplace = aceInstance.ace_getRep();
3060
- // log(`${callAceLogPrefix} Fetched rep after replace for applyMeta. Line ${lineNum} text now: "${repAfterReplace.lines.atIndex(lineNum).text}"`);
3502
+ // log(`${callAceLogPrefix} Fetched rep after replace for applyMeta. Line ${lineNum} text now: "${repAfterReplace.lines.atIndex(lineNum).text}"`);
3061
3503
 
3062
3504
  ed.ep_data_tables_applyMeta(
3063
3505
  lineNum,
@@ -3069,17 +3511,17 @@ exports.aceInitialized = (h, ctx) => {
3069
3511
  null,
3070
3512
  docManager
3071
3513
  );
3072
- // log(`${callAceLogPrefix} tbljson attribute re-applied successfully via ep_data_tables_applyMeta.`);
3514
+ // log(`${callAceLogPrefix} tbljson attribute re-applied successfully via ep_data_tables_applyMeta.`);
3073
3515
 
3074
3516
  const newCaretCol = selStart[1] + pastedText.length;
3075
3517
  const newCaretPos = [lineNum, newCaretCol];
3076
- // log(`${callAceLogPrefix} New calculated caret position: [${newCaretPos}]. Setting selection.`);
3518
+ // log(`${callAceLogPrefix} New calculated caret position: [${newCaretPos}]. Setting selection.`);
3077
3519
  aceInstance.ace_performSelectionChange(newCaretPos, newCaretPos, false);
3078
- // log(`${callAceLogPrefix} Selection change successful.`);
3520
+ // log(`${callAceLogPrefix} Selection change successful.`);
3079
3521
 
3080
- // log(`${callAceLogPrefix} Requesting fastIncorp(10) for sync.`);
3522
+ // log(`${callAceLogPrefix} Requesting fastIncorp(10) for sync.`);
3081
3523
  aceInstance.ace_fastIncorp(10);
3082
- // log(`${callAceLogPrefix} fastIncorp requested.`);
3524
+ // log(`${callAceLogPrefix} fastIncorp requested.`);
3083
3525
 
3084
3526
  // Update stored click/caret info
3085
3527
  if (editor && editor.ep_data_tables_last_clicked && editor.ep_data_tables_last_clicked.tblId === tableMetadata.tblId) {
@@ -3090,23 +3532,23 @@ exports.aceInitialized = (h, ctx) => {
3090
3532
  cellIndex: targetCellIndex,
3091
3533
  relativePos: newRelativePos < 0 ? 0 : newRelativePos,
3092
3534
  };
3093
- // log(`${callAceLogPrefix} Updated stored click/caret info:`, editor.ep_data_tables_last_clicked);
3535
+ // log(`${callAceLogPrefix} Updated stored click/caret info:`, editor.ep_data_tables_last_clicked);
3094
3536
  }
3095
3537
 
3096
- // log(`${callAceLogPrefix} Paste operations within ace_callWithAce completed successfully.`);
3538
+ // log(`${callAceLogPrefix} Paste operations within ace_callWithAce completed successfully.`);
3097
3539
  }, 'tablePasteTextOperations', true);
3098
- // log(`${pasteLogPrefix} ed.ace_callWithAce for paste operations was called.`);
3540
+ // log(`${pasteLogPrefix} ed.ace_callWithAce for paste operations was called.`);
3099
3541
 
3100
3542
  } catch (error) {
3101
3543
  console.error(`${pasteLogPrefix} CRITICAL ERROR during paste handling operation:`, error);
3102
- // log(`${pasteLogPrefix} Error details:`, { message: error.message, stack: error.stack });
3103
- // log(`${pasteLogPrefix} Paste handling FAILED. END OF HANDLER.`);
3544
+ // log(`${pasteLogPrefix} Error details:`, { message: error.message, stack: error.stack });
3545
+ // log(`${pasteLogPrefix} Paste handling FAILED. END OF HANDLER.`);
3104
3546
  }
3105
3547
  });
3106
- // log(`${callWithAceLogPrefix} Paste event listener attached.`);
3548
+ // log(`${callWithAceLogPrefix} Paste event listener attached.`);
3107
3549
 
3108
3550
  // *** NEW: Column resize listeners ***
3109
- // log(`${callWithAceLogPrefix} Attaching column resize listeners...`);
3551
+ // log(`${callWithAceLogPrefix} Attaching column resize listeners...`);
3110
3552
 
3111
3553
  // Get the iframe documents for proper event delegation
3112
3554
  const $iframeOuter = $('iframe[name="ace_outer"]');
@@ -3114,16 +3556,16 @@ exports.aceInitialized = (h, ctx) => {
3114
3556
  const innerDoc = $iframeInner.contents();
3115
3557
  const outerDoc = $iframeOuter.contents();
3116
3558
 
3117
- // log(`${callWithAceLogPrefix} Found iframe documents: outer=${outerDoc.length}, inner=${innerDoc.length}`);
3559
+ // log(`${callWithAceLogPrefix} Found iframe documents: outer=${outerDoc.length}, inner=${innerDoc.length}`);
3118
3560
 
3119
3561
  // Mousedown on resize handles
3120
3562
  $inner.on('mousedown', '.ep-data_tables-resize-handle', (evt) => {
3121
3563
  const resizeLogPrefix = '[ep_data_tables:resizeMousedown]';
3122
- // log(`${resizeLogPrefix} Resize handle mousedown detected`);
3564
+ // log(`${resizeLogPrefix} Resize handle mousedown detected`);
3123
3565
 
3124
3566
  // Only handle left mouse button clicks
3125
3567
  if (evt.button !== 0) {
3126
- // log(`${resizeLogPrefix} Ignoring non-left mouse button: ${evt.button}`);
3568
+ // log(`${resizeLogPrefix} Ignoring non-left mouse button: ${evt.button}`);
3127
3569
  return;
3128
3570
  }
3129
3571
 
@@ -3134,7 +3576,7 @@ exports.aceInitialized = (h, ctx) => {
3134
3576
  const isImageResizeHandle = $target.hasClass('image-resize-handle') || $target.closest('.image-resize-handle').length > 0;
3135
3577
 
3136
3578
  if (isImageRelated || isImageResizeHandle) {
3137
- // log(`${resizeLogPrefix} Click detected on image-related element or image resize handle, ignoring for table resize`);
3579
+ // log(`${resizeLogPrefix} Click detected on image-related element or image resize handle, ignoring for table resize`);
3138
3580
  return;
3139
3581
  }
3140
3582
 
@@ -3146,7 +3588,7 @@ exports.aceInitialized = (h, ctx) => {
3146
3588
  const table = handle.closest('table.dataTable');
3147
3589
  const lineNode = table.closest('div.ace-line');
3148
3590
 
3149
- // log(`${resizeLogPrefix} Parsed resize target: columnIndex=${columnIndex}, table=${!!table}, lineNode=${!!lineNode}`);
3591
+ // log(`${resizeLogPrefix} Parsed resize target: columnIndex=${columnIndex}, table=${!!table}, lineNode=${!!lineNode}`);
3150
3592
 
3151
3593
  if (table && lineNode && !isNaN(columnIndex)) {
3152
3594
  // Get table metadata
@@ -3160,7 +3602,7 @@ exports.aceInitialized = (h, ctx) => {
3160
3602
 
3161
3603
  const lineNum = rep.lines.indexOfKey(lineNode.id);
3162
3604
 
3163
- // log(`${resizeLogPrefix} Table info: tblId=${tblId}, lineNum=${lineNum}`);
3605
+ // log(`${resizeLogPrefix} Table info: tblId=${tblId}, lineNum=${lineNum}`);
3164
3606
 
3165
3607
  if (tblId && lineNum !== -1) {
3166
3608
  try {
@@ -3168,17 +3610,17 @@ exports.aceInitialized = (h, ctx) => {
3168
3610
  if (lineAttrString) {
3169
3611
  const metadata = JSON.parse(lineAttrString);
3170
3612
  if (metadata.tblId === tblId) {
3171
- // log(`${resizeLogPrefix} Starting resize with metadata:`, metadata);
3613
+ // log(`${resizeLogPrefix} Starting resize with metadata:`, metadata);
3172
3614
  startColumnResize(table, columnIndex, evt.clientX, metadata, lineNum);
3173
- // log(`${resizeLogPrefix} Started resize for column ${columnIndex}`);
3615
+ // log(`${resizeLogPrefix} Started resize for column ${columnIndex}`);
3174
3616
 
3175
3617
  // DEBUG: Verify global state is set
3176
- // log(`${resizeLogPrefix} Global resize state: isResizing=${isResizing}, targetTable=${!!resizeTargetTable}, targetColumn=${resizeTargetColumn}`);
3618
+ // log(`${resizeLogPrefix} Global resize state: isResizing=${isResizing}, targetTable=${!!resizeTargetTable}, targetColumn=${resizeTargetColumn}`);
3177
3619
  } else {
3178
- // log(`${resizeLogPrefix} Table ID mismatch: ${metadata.tblId} vs ${tblId}`);
3620
+ // log(`${resizeLogPrefix} Table ID mismatch: ${metadata.tblId} vs ${tblId}`);
3179
3621
  }
3180
3622
  } else {
3181
- // log(`${resizeLogPrefix} No table metadata found for line ${lineNum}, trying DOM reconstruction...`);
3623
+ // log(`${resizeLogPrefix} No table metadata found for line ${lineNum}, trying DOM reconstruction...`);
3182
3624
 
3183
3625
  // Fallback: Reconstruct metadata from DOM (same logic as ace_doDatatableOptions)
3184
3626
  const rep = ed.ace_getRep();
@@ -3212,37 +3654,37 @@ exports.aceInitialized = (h, ctx) => {
3212
3654
  cols: domCells.length,
3213
3655
  columnWidths: columnWidths
3214
3656
  };
3215
- // log(`${resizeLogPrefix} Reconstructed metadata from DOM:`, reconstructedMetadata);
3657
+ // log(`${resizeLogPrefix} Reconstructed metadata from DOM:`, reconstructedMetadata);
3216
3658
 
3217
3659
  startColumnResize(table, columnIndex, evt.clientX, reconstructedMetadata, lineNum);
3218
- // log(`${resizeLogPrefix} Started resize for column ${columnIndex} using reconstructed metadata`);
3660
+ // log(`${resizeLogPrefix} Started resize for column ${columnIndex} using reconstructed metadata`);
3219
3661
 
3220
3662
  // DEBUG: Verify global state is set
3221
- // log(`${resizeLogPrefix} Global resize state: isResizing=${isResizing}, targetTable=${!!resizeTargetTable}, targetColumn=${resizeTargetColumn}`);
3663
+ // log(`${resizeLogPrefix} Global resize state: isResizing=${isResizing}, targetTable=${!!resizeTargetTable}, targetColumn=${resizeTargetColumn}`);
3222
3664
  } else {
3223
- // log(`${resizeLogPrefix} DOM table found but no cells detected`);
3665
+ // log(`${resizeLogPrefix} DOM table found but no cells detected`);
3224
3666
  }
3225
3667
  } else {
3226
- // log(`${resizeLogPrefix} DOM table found but tblId mismatch or missing row: domTblId=${domTblId}, domRow=${domRow}`);
3668
+ // log(`${resizeLogPrefix} DOM table found but tblId mismatch or missing row: domTblId=${domTblId}, domRow=${domRow}`);
3227
3669
  }
3228
3670
  } else {
3229
- // log(`${resizeLogPrefix} No table found in DOM for line ${lineNum}`);
3671
+ // log(`${resizeLogPrefix} No table found in DOM for line ${lineNum}`);
3230
3672
  }
3231
3673
  } else {
3232
- // log(`${resizeLogPrefix} Could not get line entry or lineNode for line ${lineNum}`);
3674
+ // log(`${resizeLogPrefix} Could not get line entry or lineNode for line ${lineNum}`);
3233
3675
  }
3234
3676
  } else {
3235
- // log(`${resizeLogPrefix} Could not get rep or rep.lines for DOM reconstruction`);
3677
+ // log(`${resizeLogPrefix} Could not get rep or rep.lines for DOM reconstruction`);
3236
3678
  }
3237
3679
  }
3238
3680
  } catch (e) {
3239
3681
  console.error(`${resizeLogPrefix} Error getting table metadata:`, e);
3240
3682
  }
3241
3683
  } else {
3242
- // log(`${resizeLogPrefix} Invalid line number (${lineNum}) or table ID (${tblId})`);
3684
+ // log(`${resizeLogPrefix} Invalid line number (${lineNum}) or table ID (${tblId})`);
3243
3685
  }
3244
3686
  } else {
3245
- // log(`${resizeLogPrefix} Invalid resize target:`, { table: !!table, lineNode: !!lineNode, columnIndex });
3687
+ // log(`${resizeLogPrefix} Invalid resize target:`, { table: !!table, lineNode: !!lineNode, columnIndex });
3246
3688
  }
3247
3689
  });
3248
3690
 
@@ -3261,57 +3703,57 @@ exports.aceInitialized = (h, ctx) => {
3261
3703
 
3262
3704
  // Mouseup handler with enhanced debugging
3263
3705
  const handleMouseup = (evt) => {
3264
- // log(`${mouseupLogPrefix} Mouseup detected on ${evt.target.tagName || 'unknown'}. isResizing: ${isResizing}`);
3706
+ // log(`${mouseupLogPrefix} Mouseup detected on ${evt.target.tagName || 'unknown'}. isResizing: ${isResizing}`);
3265
3707
 
3266
3708
  if (isResizing) {
3267
- // log(`${mouseupLogPrefix} Processing resize completion...`);
3709
+ // log(`${mouseupLogPrefix} Processing resize completion...`);
3268
3710
  evt.preventDefault();
3269
3711
  evt.stopPropagation();
3270
3712
 
3271
3713
  // Add a small delay to ensure all DOM updates are complete
3272
3714
  setTimeout(() => {
3273
- // log(`${mouseupLogPrefix} Executing finishColumnResize after delay...`);
3715
+ // log(`${mouseupLogPrefix} Executing finishColumnResize after delay...`);
3274
3716
  finishColumnResize(ed, docManager);
3275
- // log(`${mouseupLogPrefix} Resize completion finished.`);
3717
+ // log(`${mouseupLogPrefix} Resize completion finished.`);
3276
3718
  }, 50);
3277
3719
  } else {
3278
- // log(`${mouseupLogPrefix} Not in resize mode, ignoring mouseup.`);
3720
+ // log(`${mouseupLogPrefix} Not in resize mode, ignoring mouseup.`);
3279
3721
  }
3280
3722
  };
3281
3723
 
3282
3724
  // Attach to multiple contexts to ensure we catch the event
3283
- // log(`${callWithAceLogPrefix} Attaching global mousemove/mouseup handlers to multiple contexts...`);
3725
+ // log(`${callWithAceLogPrefix} Attaching global mousemove/mouseup handlers to multiple contexts...`);
3284
3726
 
3285
3727
  // 1. Main document (outside iframes)
3286
3728
  $(document).on('mousemove', handleMousemove);
3287
3729
  $(document).on('mouseup', handleMouseup);
3288
- // log(`${callWithAceLogPrefix} Attached to main document`);
3730
+ // log(`${callWithAceLogPrefix} Attached to main document`);
3289
3731
 
3290
3732
  // 2. Outer iframe document
3291
3733
  if (outerDoc.length > 0) {
3292
3734
  outerDoc.on('mousemove', handleMousemove);
3293
3735
  outerDoc.on('mouseup', handleMouseup);
3294
- // log(`${callWithAceLogPrefix} Attached to outer iframe document`);
3736
+ // log(`${callWithAceLogPrefix} Attached to outer iframe document`);
3295
3737
  }
3296
3738
 
3297
3739
  // 3. Inner iframe document
3298
3740
  if (innerDoc.length > 0) {
3299
3741
  innerDoc.on('mousemove', handleMousemove);
3300
3742
  innerDoc.on('mouseup', handleMouseup);
3301
- // log(`${callWithAceLogPrefix} Attached to inner iframe document`);
3743
+ // log(`${callWithAceLogPrefix} Attached to inner iframe document`);
3302
3744
  }
3303
3745
 
3304
3746
  // 4. Inner iframe body (the editing area)
3305
3747
  $inner.on('mousemove', handleMousemove);
3306
3748
  $inner.on('mouseup', handleMouseup);
3307
- // log(`${callWithAceLogPrefix} Attached to inner iframe body`);
3749
+ // log(`${callWithAceLogPrefix} Attached to inner iframe body`);
3308
3750
 
3309
3751
  // 5. Add a failsafe - listen for any mouse events during resize
3310
3752
  const failsafeMouseup = (evt) => {
3311
3753
  if (isResizing) {
3312
- // log(`${mouseupLogPrefix} FAILSAFE: Detected mouse event during resize: ${evt.type}`);
3754
+ // log(`${mouseupLogPrefix} FAILSAFE: Detected mouse event during resize: ${evt.type}`);
3313
3755
  if (evt.type === 'mouseup' || evt.type === 'mousedown' || evt.type === 'click') {
3314
- // log(`${mouseupLogPrefix} FAILSAFE: Triggering resize completion due to ${evt.type}`);
3756
+ // log(`${mouseupLogPrefix} FAILSAFE: Triggering resize completion due to ${evt.type}`);
3315
3757
  setTimeout(() => {
3316
3758
  if (isResizing) { // Double-check we're still resizing
3317
3759
  finishColumnResize(ed, docManager);
@@ -3325,7 +3767,7 @@ exports.aceInitialized = (h, ctx) => {
3325
3767
  document.addEventListener('mouseup', failsafeMouseup, true);
3326
3768
  document.addEventListener('mousedown', failsafeMouseup, true);
3327
3769
  document.addEventListener('click', failsafeMouseup, true);
3328
- // log(`${callWithAceLogPrefix} Attached failsafe event handlers`);
3770
+ // log(`${callWithAceLogPrefix} Attached failsafe event handlers`);
3329
3771
 
3330
3772
  // *** DRAG PREVENTION FOR TABLE ELEMENTS ***
3331
3773
  const preventTableDrag = (evt) => {
@@ -3333,7 +3775,7 @@ exports.aceInitialized = (h, ctx) => {
3333
3775
  // Block ANY drag gesture that originates within a table (including spans/text inside cells)
3334
3776
  const inTable = target && typeof target.closest === 'function' && target.closest('table.dataTable');
3335
3777
  if (inTable) {
3336
- // log('[ep_data_tables:dragPrevention] Preventing drag operation originating from inside table');
3778
+ // log('[ep_data_tables:dragPrevention] Preventing drag operation originating from inside table');
3337
3779
  evt.preventDefault();
3338
3780
  evt.stopPropagation();
3339
3781
  if (evt.originalEvent && evt.originalEvent.dataTransfer) {
@@ -3347,7 +3789,7 @@ exports.aceInitialized = (h, ctx) => {
3347
3789
  $inner.on('dragstart', preventTableDrag);
3348
3790
  $inner.on('drag', preventTableDrag);
3349
3791
  $inner.on('dragend', preventTableDrag);
3350
- // log(`${callWithAceLogPrefix} Attached drag prevention handlers to inner body`);
3792
+ // log(`${callWithAceLogPrefix} Attached drag prevention handlers to inner body`);
3351
3793
 
3352
3794
  // Attach drag prevention broadly to cover iframe boundaries and the host document
3353
3795
  if (innerDoc.length > 0) {
@@ -3368,15 +3810,15 @@ exports.aceInitialized = (h, ctx) => {
3368
3810
  // Setup the global handlers
3369
3811
  setupGlobalHandlers();
3370
3812
 
3371
- // log(`${callWithAceLogPrefix} Column resize listeners attached successfully.`);
3813
+ // log(`${callWithAceLogPrefix} Column resize listeners attached successfully.`);
3372
3814
 
3373
3815
  }, 'tablePasteAndResizeListeners', true);
3374
- // log(`${logPrefix} ace_callWithAce for listeners setup completed.`);
3816
+ // log(`${logPrefix} ace_callWithAce for listeners setup completed.`);
3375
3817
 
3376
3818
  // Helper function to apply the metadata attribute to a line
3377
3819
  function applyTableLineMetadataAttribute (lineNum, tblId, rowIndex, numCols, rep, editorInfo, attributeString = null, documentAttributeManager = null) {
3378
3820
  const funcName = 'applyTableLineMetadataAttribute';
3379
- // log(`${logPrefix}:${funcName}: START - Applying METADATA attribute to line ${lineNum}`, {tblId, rowIndex, numCols});
3821
+ // log(`${logPrefix}:${funcName}: START - Applying METADATA attribute to line ${lineNum}`, {tblId, rowIndex, numCols});
3380
3822
 
3381
3823
  let finalMetadata;
3382
3824
 
@@ -3387,14 +3829,14 @@ exports.aceInitialized = (h, ctx) => {
3387
3829
  if (providedMetadata.columnWidths && Array.isArray(providedMetadata.columnWidths) && providedMetadata.columnWidths.length === numCols) {
3388
3830
  // Already has valid columnWidths, use as-is
3389
3831
  finalMetadata = providedMetadata;
3390
- // log(`${logPrefix}:${funcName}: Using provided metadata with existing columnWidths`);
3832
+ // log(`${logPrefix}:${funcName}: Using provided metadata with existing columnWidths`);
3391
3833
  } else {
3392
3834
  // Has metadata but missing/invalid columnWidths, extract from DOM
3393
3835
  finalMetadata = providedMetadata;
3394
- // log(`${logPrefix}:${funcName}: Provided metadata missing columnWidths, attempting DOM extraction`);
3836
+ // log(`${logPrefix}:${funcName}: Provided metadata missing columnWidths, attempting DOM extraction`);
3395
3837
  }
3396
3838
  } catch (e) {
3397
- // log(`${logPrefix}:${funcName}: Error parsing provided attributeString, will reconstruct:`, e);
3839
+ // log(`${logPrefix}:${funcName}: Error parsing provided attributeString, will reconstruct:`, e);
3398
3840
  finalMetadata = null;
3399
3841
  }
3400
3842
  }
@@ -3425,13 +3867,13 @@ exports.aceInitialized = (h, ctx) => {
3425
3867
  columnWidths.push(100 / numCols);
3426
3868
  }
3427
3869
  });
3428
- // log(`${logPrefix}:${funcName}: Extracted column widths from DOM: ${columnWidths.map(w => w.toFixed(1) + '%').join(', ')}`);
3870
+ // log(`${logPrefix}:${funcName}: Extracted column widths from DOM: ${columnWidths.map(w => w.toFixed(1) + '%').join(', ')}`);
3429
3871
  }
3430
3872
  }
3431
3873
  }
3432
3874
  }
3433
3875
  } catch (e) {
3434
- // log(`${logPrefix}:${funcName}: Error extracting column widths from DOM:`, e);
3876
+ // log(`${logPrefix}:${funcName}: Error extracting column widths from DOM:`, e);
3435
3877
  }
3436
3878
 
3437
3879
  // Build final metadata
@@ -3448,26 +3890,26 @@ exports.aceInitialized = (h, ctx) => {
3448
3890
  }
3449
3891
 
3450
3892
  const finalAttributeString = JSON.stringify(finalMetadata);
3451
- // log(`${logPrefix}:${funcName}: Final metadata attribute string: ${finalAttributeString}`);
3893
+ // log(`${logPrefix}:${funcName}: Final metadata attribute string: ${finalAttributeString}`);
3452
3894
 
3453
3895
  try {
3454
3896
  // Get current line info
3455
3897
  const lineEntry = rep.lines.atIndex(lineNum);
3456
3898
  if (!lineEntry) {
3457
- // log(`${logPrefix}:${funcName}: ERROR - Could not find line entry for line number ${lineNum}`);
3899
+ // log(`${logPrefix}:${funcName}: ERROR - Could not find line entry for line number ${lineNum}`);
3458
3900
  return;
3459
3901
  }
3460
3902
  const lineLength = Math.max(1, lineEntry.text.length);
3461
- // log(`${logPrefix}:${funcName}: Line ${lineNum} text length: ${lineLength}`);
3903
+ // log(`${logPrefix}:${funcName}: Line ${lineNum} text length: ${lineLength}`);
3462
3904
 
3463
3905
  // Simple attribute application - just add the tbljson attribute
3464
3906
  const attributes = [[ATTR_TABLE_JSON, finalAttributeString]];
3465
3907
  const start = [lineNum, 0];
3466
3908
  const end = [lineNum, lineLength];
3467
3909
 
3468
- // log(`${logPrefix}:${funcName}: Applying tbljson attribute to range [${start}]-[${end}]`);
3910
+ // log(`${logPrefix}:${funcName}: Applying tbljson attribute to range [${start}]-[${end}]`);
3469
3911
  editorInfo.ace_performDocumentApplyAttributesToRange(start, end, attributes);
3470
- // log(`${logPrefix}:${funcName}: Successfully applied tbljson attribute to line ${lineNum}`);
3912
+ // log(`${logPrefix}:${funcName}: Successfully applied tbljson attribute to line ${lineNum}`);
3471
3913
 
3472
3914
  } catch(e) {
3473
3915
  console.error(`[ep_data_tables] ${logPrefix}:${funcName}: Error applying metadata attribute on line ${lineNum}:`, e);
@@ -3477,59 +3919,59 @@ exports.aceInitialized = (h, ctx) => {
3477
3919
  /** Insert a fresh rows×cols blank table at the caret */
3478
3920
  ed.ace_createTableViaAttributes = (rows = 2, cols = 2) => {
3479
3921
  const funcName = 'ace_createTableViaAttributes';
3480
- // log(`${funcName}: START - Refactored Phase 4 (Get Selection Fix)`, { rows, cols });
3922
+ // log(`${funcName}: START - Refactored Phase 4 (Get Selection Fix)`, { rows, cols });
3481
3923
  rows = Math.max(1, rows); cols = Math.max(1, cols);
3482
- // log(`${funcName}: Ensuring minimum 1 row, 1 col.`);
3924
+ // log(`${funcName}: Ensuring minimum 1 row, 1 col.`);
3483
3925
 
3484
3926
  // --- Phase 1: Prepare Data ---
3485
3927
  const tblId = rand();
3486
- // log(`${funcName}: Generated table ID: ${tblId}`);
3928
+ // log(`${funcName}: Generated table ID: ${tblId}`);
3487
3929
  const initialCellContent = ' '; // Start with a single space per cell
3488
3930
  const lineTxt = Array.from({ length: cols }).fill(initialCellContent).join(DELIMITER);
3489
- // log(`${funcName}: Constructed initial line text for ${cols} cols: "${lineTxt}"`);
3931
+ // log(`${funcName}: Constructed initial line text for ${cols} cols: "${lineTxt}"`);
3490
3932
  const block = Array.from({ length: rows }).fill(lineTxt).join('\n') + '\n';
3491
- // log(`${funcName}: Constructed block for ${rows} rows:\n${block}`);
3933
+ // log(`${funcName}: Constructed block for ${rows} rows:\n${block}`);
3492
3934
 
3493
3935
  // Get current selection BEFORE making changes using ace_getRep()
3494
- // log(`${funcName}: Getting current representation and selection...`);
3936
+ // log(`${funcName}: Getting current representation and selection...`);
3495
3937
  const currentRepInitial = ed.ace_getRep();
3496
3938
  if (!currentRepInitial || !currentRepInitial.selStart || !currentRepInitial.selEnd) {
3497
3939
  console.error(`[ep_data_tables] ${funcName}: Could not get current representation or selection via ace_getRep(). Aborting.`);
3498
- // log(`${funcName}: END - Error getting initial rep/selection`);
3940
+ // log(`${funcName}: END - Error getting initial rep/selection`);
3499
3941
  return;
3500
3942
  }
3501
3943
  const start = currentRepInitial.selStart;
3502
3944
  const end = currentRepInitial.selEnd;
3503
3945
  const initialStartLine = start[0]; // Store the starting line number
3504
- // log(`${funcName}: Current selection from initial rep:`, { start, end });
3946
+ // log(`${funcName}: Current selection from initial rep:`, { start, end });
3505
3947
 
3506
3948
  // --- Phase 2: Insert Text Block ---
3507
- // log(`${funcName}: Phase 2 - Inserting text block...`);
3949
+ // log(`${funcName}: Phase 2 - Inserting text block...`);
3508
3950
  ed.ace_performDocumentReplaceRange(start, end, block);
3509
- // log(`${funcName}: Inserted block of delimited text lines.`);
3510
- // log(`${funcName}: Requesting text sync (ace_fastIncorp 20)...`);
3951
+ // log(`${funcName}: Inserted block of delimited text lines.`);
3952
+ // log(`${funcName}: Requesting text sync (ace_fastIncorp 20)...`);
3511
3953
  ed.ace_fastIncorp(20); // Sync text insertion
3512
- // log(`${funcName}: Text sync requested.`);
3954
+ // log(`${funcName}: Text sync requested.`);
3513
3955
 
3514
3956
  // --- Phase 3: Apply Metadata Attributes ---
3515
- // log(`${funcName}: Phase 3 - Applying metadata attributes to ${rows} inserted lines...`);
3957
+ // log(`${funcName}: Phase 3 - Applying metadata attributes to ${rows} inserted lines...`);
3516
3958
  // Need rep to be updated after text insertion to apply attributes correctly
3517
3959
  const currentRep = ed.ace_getRep(); // Get potentially updated rep
3518
3960
  if (!currentRep || !currentRep.lines) {
3519
3961
  console.error(`[ep_data_tables] ${funcName}: Could not get updated rep after text insertion. Cannot apply attributes reliably.`);
3520
- // log(`${funcName}: END - Error getting updated rep`);
3962
+ // log(`${funcName}: END - Error getting updated rep`);
3521
3963
  // Maybe attempt to continue without rep? Risky.
3522
3964
  return;
3523
3965
  }
3524
- // log(`${funcName}: Fetched updated rep for attribute application.`);
3966
+ // log(`${funcName}: Fetched updated rep for attribute application.`);
3525
3967
 
3526
3968
  for (let r = 0; r < rows; r++) {
3527
3969
  const lineNumToApply = initialStartLine + r;
3528
- // log(`${funcName}: -> Processing row ${r} on line ${lineNumToApply}`);
3970
+ // log(`${funcName}: -> Processing row ${r} on line ${lineNumToApply}`);
3529
3971
 
3530
3972
  const lineEntry = currentRep.lines.atIndex(lineNumToApply);
3531
3973
  if (!lineEntry) {
3532
- // log(`${funcName}: Could not find line entry for ${lineNumToApply}, skipping attribute application.`);
3974
+ // log(`${funcName}: Could not find line entry for ${lineNumToApply}, skipping attribute application.`);
3533
3975
  continue;
3534
3976
  }
3535
3977
  const lineText = lineEntry.text || '';
@@ -3542,7 +3984,7 @@ exports.aceInitialized = (h, ctx) => {
3542
3984
  if (cellContent.length > 0) { // Only apply to non-empty cells
3543
3985
  const cellStart = [lineNumToApply, offset];
3544
3986
  const cellEnd = [lineNumToApply, offset + cellContent.length];
3545
- // log(`${funcName}: Applying ${ATTR_CELL} attribute to Line ${lineNumToApply} Col ${c} Range ${offset}-${offset + cellContent.length}`);
3987
+ // log(`${funcName}: Applying ${ATTR_CELL} attribute to Line ${lineNumToApply} Col ${c} Range ${offset}-${offset + cellContent.length}`);
3546
3988
  ed.ace_performDocumentApplyAttributesToRange(cellStart, cellEnd, [[ATTR_CELL, String(c)]]);
3547
3989
  }
3548
3990
  offset += cellContent.length;
@@ -3555,30 +3997,30 @@ exports.aceInitialized = (h, ctx) => {
3555
3997
  // Note: documentAttributeManager not available in this context for new table creation
3556
3998
  applyTableLineMetadataAttribute(lineNumToApply, tblId, r, cols, currentRep, ed, null, null);
3557
3999
  }
3558
- // log(`${funcName}: Finished applying metadata attributes.`);
3559
- // log(`${funcName}: Requesting attribute sync (ace_fastIncorp 20)...`);
4000
+ // log(`${funcName}: Finished applying metadata attributes.`);
4001
+ // log(`${funcName}: Requesting attribute sync (ace_fastIncorp 20)...`);
3560
4002
  ed.ace_fastIncorp(20); // Final sync after attributes
3561
- // log(`${funcName}: Attribute sync requested.`);
4003
+ // log(`${funcName}: Attribute sync requested.`);
3562
4004
 
3563
4005
  // --- Phase 4: Set Caret Position ---
3564
- // log(`${funcName}: Phase 4 - Setting final caret position...`);
4006
+ // log(`${funcName}: Phase 4 - Setting final caret position...`);
3565
4007
  const finalCaretLine = initialStartLine + rows; // Line number after the last inserted row
3566
4008
  const finalCaretPos = [finalCaretLine, 0];
3567
- // log(`${funcName}: Target caret position:`, finalCaretPos);
4009
+ // log(`${funcName}: Target caret position:`, finalCaretPos);
3568
4010
  try {
3569
4011
  ed.ace_performSelectionChange(finalCaretPos, finalCaretPos, false);
3570
- // log(`${funcName}: Successfully set caret position.`);
4012
+ // log(`${funcName}: Successfully set caret position.`);
3571
4013
  } catch(e) {
3572
4014
  console.error(`[ep_data_tables] ${funcName}: Error setting caret position after table creation:`, e);
3573
- // log(`[ep_data_tables] ${funcName}: Error details:`, { message: e.message, stack: e.stack });
4015
+ // log(`[ep_data_tables] ${funcName}: Error details:`, { message: e.message, stack: e.stack });
3574
4016
  }
3575
4017
 
3576
- // log(`${funcName}: END - Refactored Phase 4`);
4018
+ // log(`${funcName}: END - Refactored Phase 4`);
3577
4019
  };
3578
4020
 
3579
4021
  ed.ace_doDatatableOptions = (action) => {
3580
4022
  const funcName = 'ace_doDatatableOptions';
3581
- // log(`${funcName}: START - Processing action: ${action}`);
4023
+ // log(`${funcName}: START - Processing action: ${action}`);
3582
4024
 
3583
4025
  // Get the last clicked cell info to determine which table to operate on
3584
4026
  const editor = ed.ep_data_tables_editor;
@@ -3589,12 +4031,12 @@ exports.aceInitialized = (h, ctx) => {
3589
4031
 
3590
4032
  const lastClick = editor.ep_data_tables_last_clicked;
3591
4033
  if (!lastClick || !lastClick.tblId) {
3592
- // log(`${funcName}: No table selected. Please click on a table cell first.`);
4034
+ // log(`${funcName}: No table selected. Please click on a table cell first.`);
3593
4035
  console.warn('[ep_data_tables] No table selected. Please click on a table cell first.');
3594
4036
  return;
3595
4037
  }
3596
4038
 
3597
- // log(`${funcName}: Operating on table ${lastClick.tblId}, clicked line ${lastClick.lineNum}, cell ${lastClick.cellIndex}`);
4039
+ // log(`${funcName}: Operating on table ${lastClick.tblId}, clicked line ${lastClick.lineNum}, cell ${lastClick.cellIndex}`);
3598
4040
 
3599
4041
  try {
3600
4042
  // Get current representation and document manager
@@ -3611,7 +4053,7 @@ exports.aceInitialized = (h, ctx) => {
3611
4053
  return;
3612
4054
  }
3613
4055
 
3614
- // log(`${funcName}: Successfully obtained documentAttributeManager from stored reference.`);
4056
+ // log(`${funcName}: Successfully obtained documentAttributeManager from stored reference.`);
3615
4057
 
3616
4058
  // Find all lines that belong to this table
3617
4059
  const tableLines = [];
@@ -3640,7 +4082,7 @@ exports.aceInitialized = (h, ctx) => {
3640
4082
  cols: domCells.length
3641
4083
  };
3642
4084
  lineAttrString = JSON.stringify(reconstructedMetadata);
3643
- // log(`${funcName}: Reconstructed metadata from DOM for line ${lineIndex}: ${lineAttrString}`);
4085
+ // log(`${funcName}: Reconstructed metadata from DOM for line ${lineIndex}: ${lineAttrString}`);
3644
4086
  }
3645
4087
  }
3646
4088
  }
@@ -3668,13 +4110,13 @@ exports.aceInitialized = (h, ctx) => {
3668
4110
  }
3669
4111
 
3670
4112
  if (tableLines.length === 0) {
3671
- // log(`${funcName}: No table lines found for table ${lastClick.tblId}`);
4113
+ // log(`${funcName}: No table lines found for table ${lastClick.tblId}`);
3672
4114
  return;
3673
4115
  }
3674
4116
 
3675
4117
  // Sort by row number to ensure correct order
3676
4118
  tableLines.sort((a, b) => a.row - b.row);
3677
- // log(`${funcName}: Found ${tableLines.length} table lines`);
4119
+ // log(`${funcName}: Found ${tableLines.length} table lines`);
3678
4120
 
3679
4121
  // Determine table dimensions and target indices with robust matching
3680
4122
  const numRows = tableLines.length;
@@ -3688,7 +4130,7 @@ exports.aceInitialized = (h, ctx) => {
3688
4130
 
3689
4131
  // If that fails, try to match by finding the row that contains the clicked table
3690
4132
  if (targetRowIndex === -1) {
3691
- // log(`${funcName}: Direct line number match failed, searching by DOM structure...`);
4133
+ // log(`${funcName}: Direct line number match failed, searching by DOM structure...`);
3692
4134
  const clickedLineEntry = currentRep.lines.atIndex(lastClick.lineNum);
3693
4135
  if (clickedLineEntry && clickedLineEntry.lineNode) {
3694
4136
  const clickedTable = clickedLineEntry.lineNode.querySelector('table.dataTable[data-tblId="' + lastClick.tblId + '"]');
@@ -3697,7 +4139,7 @@ exports.aceInitialized = (h, ctx) => {
3697
4139
  if (clickedRowAttr !== null) {
3698
4140
  const clickedRowNum = parseInt(clickedRowAttr, 10);
3699
4141
  targetRowIndex = tableLines.findIndex(line => line.row === clickedRowNum);
3700
- // log(`${funcName}: Found target row by DOM attribute matching: row ${clickedRowNum}, index ${targetRowIndex}`);
4142
+ // log(`${funcName}: Found target row by DOM attribute matching: row ${clickedRowNum}, index ${targetRowIndex}`);
3701
4143
  }
3702
4144
  }
3703
4145
  }
@@ -3705,13 +4147,13 @@ exports.aceInitialized = (h, ctx) => {
3705
4147
 
3706
4148
  // If still not found, default to first row but log the issue
3707
4149
  if (targetRowIndex === -1) {
3708
- // log(`${funcName}: Warning: Could not find target row, defaulting to row 0`);
4150
+ // log(`${funcName}: Warning: Could not find target row, defaulting to row 0`);
3709
4151
  targetRowIndex = 0;
3710
4152
  }
3711
4153
 
3712
4154
  const targetColIndex = lastClick.cellIndex || 0;
3713
4155
 
3714
- // log(`${funcName}: Table dimensions: ${numRows} rows x ${numCols} cols. Target: row ${targetRowIndex}, col ${targetColIndex}`);
4156
+ // log(`${funcName}: Table dimensions: ${numRows} rows x ${numCols} cols. Target: row ${targetRowIndex}, col ${targetColIndex}`);
3715
4157
 
3716
4158
  // Perform table operations with both text and metadata updates
3717
4159
  let newNumCols = numCols;
@@ -3719,23 +4161,23 @@ exports.aceInitialized = (h, ctx) => {
3719
4161
 
3720
4162
  switch (action) {
3721
4163
  case 'addTblRowA': // Insert row above
3722
- // log(`${funcName}: Inserting row above row ${targetRowIndex}`);
4164
+ // log(`${funcName}: Inserting row above row ${targetRowIndex}`);
3723
4165
  success = addTableRowAboveWithText(tableLines, targetRowIndex, numCols, lastClick.tblId, ed, docManager);
3724
4166
  break;
3725
4167
 
3726
4168
  case 'addTblRowB': // Insert row below
3727
- // log(`${funcName}: Inserting row below row ${targetRowIndex}`);
4169
+ // log(`${funcName}: Inserting row below row ${targetRowIndex}`);
3728
4170
  success = addTableRowBelowWithText(tableLines, targetRowIndex, numCols, lastClick.tblId, ed, docManager);
3729
4171
  break;
3730
4172
 
3731
4173
  case 'addTblColL': // Insert column left
3732
- // log(`${funcName}: Inserting column left of column ${targetColIndex}`);
4174
+ // log(`${funcName}: Inserting column left of column ${targetColIndex}`);
3733
4175
  newNumCols = numCols + 1;
3734
4176
  success = addTableColumnLeftWithText(tableLines, targetColIndex, ed, docManager);
3735
4177
  break;
3736
4178
 
3737
4179
  case 'addTblColR': // Insert column right
3738
- // log(`${funcName}: Inserting column right of column ${targetColIndex}`);
4180
+ // log(`${funcName}: Inserting column right of column ${targetColIndex}`);
3739
4181
  newNumCols = numCols + 1;
3740
4182
  success = addTableColumnRightWithText(tableLines, targetColIndex, ed, docManager);
3741
4183
  break;
@@ -3744,10 +4186,10 @@ exports.aceInitialized = (h, ctx) => {
3744
4186
  // Show confirmation prompt for row deletion
3745
4187
  const rowConfirmMessage = `Are you sure you want to delete Row ${targetRowIndex + 1} and all content within?`;
3746
4188
  if (!confirm(rowConfirmMessage)) {
3747
- // log(`${funcName}: Row deletion cancelled by user`);
4189
+ // log(`${funcName}: Row deletion cancelled by user`);
3748
4190
  return;
3749
4191
  }
3750
- // log(`${funcName}: Deleting row ${targetRowIndex}`);
4192
+ // log(`${funcName}: Deleting row ${targetRowIndex}`);
3751
4193
  success = deleteTableRowWithText(tableLines, targetRowIndex, ed, docManager);
3752
4194
  break;
3753
4195
 
@@ -3755,16 +4197,16 @@ exports.aceInitialized = (h, ctx) => {
3755
4197
  // Show confirmation prompt for column deletion
3756
4198
  const colConfirmMessage = `Are you sure you want to delete Column ${targetColIndex + 1} and all content within?`;
3757
4199
  if (!confirm(colConfirmMessage)) {
3758
- // log(`${funcName}: Column deletion cancelled by user`);
4200
+ // log(`${funcName}: Column deletion cancelled by user`);
3759
4201
  return;
3760
4202
  }
3761
- // log(`${funcName}: Deleting column ${targetColIndex}`);
4203
+ // log(`${funcName}: Deleting column ${targetColIndex}`);
3762
4204
  newNumCols = numCols - 1;
3763
4205
  success = deleteTableColumnWithText(tableLines, targetColIndex, ed, docManager);
3764
4206
  break;
3765
4207
 
3766
4208
  default:
3767
- // log(`${funcName}: Unknown action: ${action}`);
4209
+ // log(`${funcName}: Unknown action: ${action}`);
3768
4210
  return;
3769
4211
  }
3770
4212
 
@@ -3773,11 +4215,11 @@ exports.aceInitialized = (h, ctx) => {
3773
4215
  return;
3774
4216
  }
3775
4217
 
3776
- // log(`${funcName}: Table operation completed successfully with text and metadata synchronization`);
4218
+ // log(`${funcName}: Table operation completed successfully with text and metadata synchronization`);
3777
4219
 
3778
4220
  } catch (error) {
3779
4221
  console.error(`[ep_data_tables] ${funcName}: Error during table operation:`, error);
3780
- // log(`${funcName}: Error details:`, { message: error.message, stack: error.stack });
4222
+ // log(`${funcName}: Error details:`, { message: error.message, stack: error.stack });
3781
4223
  }
3782
4224
  };
3783
4225
 
@@ -3830,7 +4272,7 @@ exports.aceInitialized = (h, ctx) => {
3830
4272
  columnWidths.push(100 / numCols);
3831
4273
  }
3832
4274
  });
3833
- // log('[ep_data_tables] addTableRowAbove: Extracted column widths from DOM:', columnWidths);
4275
+ // log('[ep_data_tables] addTableRowAbove: Extracted column widths from DOM:', columnWidths);
3834
4276
  }
3835
4277
  }
3836
4278
  }
@@ -3908,7 +4350,7 @@ exports.aceInitialized = (h, ctx) => {
3908
4350
  columnWidths.push(100 / numCols);
3909
4351
  }
3910
4352
  });
3911
- // log('[ep_data_tables] addTableRowBelow: Extracted column widths from DOM:', columnWidths);
4353
+ // log('[ep_data_tables] addTableRowBelow: Extracted column widths from DOM:', columnWidths);
3912
4354
  }
3913
4355
  }
3914
4356
  }
@@ -3973,7 +4415,7 @@ exports.aceInitialized = (h, ctx) => {
3973
4415
  if (cellContent.length > 0) { // Only apply to non-empty cells
3974
4416
  const cellStart = [tableLine.lineIndex, offset];
3975
4417
  const cellEnd = [tableLine.lineIndex, offset + cellContent.length];
3976
- // log(`[ep_data_tables] ${funcName}: Applying ${ATTR_CELL} attribute to Line ${tableLine.lineIndex} Col ${c} Range ${offset}-${offset + cellContent.length}`);
4418
+ // log(`[ep_data_tables] ${funcName}: Applying ${ATTR_CELL} attribute to Line ${tableLine.lineIndex} Col ${c} Range ${offset}-${offset + cellContent.length}`);
3977
4419
  editorInfo.ace_performDocumentApplyAttributesToRange(cellStart, cellEnd, [[ATTR_CELL, String(c)]]);
3978
4420
  }
3979
4421
  offset += cellContent.length;
@@ -3988,7 +4430,7 @@ exports.aceInitialized = (h, ctx) => {
3988
4430
  const newColCount = tableLine.cols + 1;
3989
4431
  const equalWidth = 100 / newColCount;
3990
4432
  const normalizedWidths = Array(newColCount).fill(equalWidth);
3991
- // log(`[ep_data_tables] addTableColumnLeft: Reset all column widths to equal distribution: ${newColCount} columns at ${equalWidth.toFixed(1)}% each`);
4433
+ // log(`[ep_data_tables] addTableColumnLeft: Reset all column widths to equal distribution: ${newColCount} columns at ${equalWidth.toFixed(1)}% each`);
3992
4434
 
3993
4435
  // Apply updated metadata
3994
4436
  const newMetadata = { ...tableLine.metadata, cols: tableLine.cols + 1, columnWidths: normalizedWidths };
@@ -4040,7 +4482,7 @@ exports.aceInitialized = (h, ctx) => {
4040
4482
  if (cellContent.length > 0) { // Only apply to non-empty cells
4041
4483
  const cellStart = [tableLine.lineIndex, offset];
4042
4484
  const cellEnd = [tableLine.lineIndex, offset + cellContent.length];
4043
- // log(`[ep_data_tables] ${funcName}: Applying ${ATTR_CELL} attribute to Line ${tableLine.lineIndex} Col ${c} Range ${offset}-${offset + cellContent.length}`);
4485
+ // log(`[ep_data_tables] ${funcName}: Applying ${ATTR_CELL} attribute to Line ${tableLine.lineIndex} Col ${c} Range ${offset}-${offset + cellContent.length}`);
4044
4486
  editorInfo.ace_performDocumentApplyAttributesToRange(cellStart, cellEnd, [[ATTR_CELL, String(c)]]);
4045
4487
  }
4046
4488
  offset += cellContent.length;
@@ -4055,7 +4497,7 @@ exports.aceInitialized = (h, ctx) => {
4055
4497
  const newColCount = tableLine.cols + 1;
4056
4498
  const equalWidth = 100 / newColCount;
4057
4499
  const normalizedWidths = Array(newColCount).fill(equalWidth);
4058
- // log(`[ep_data_tables] addTableColumnRight: Reset all column widths to equal distribution: ${newColCount} columns at ${equalWidth.toFixed(1)}% each`);
4500
+ // log(`[ep_data_tables] addTableColumnRight: Reset all column widths to equal distribution: ${newColCount} columns at ${equalWidth.toFixed(1)}% each`);
4059
4501
 
4060
4502
  // Apply updated metadata
4061
4503
  const newMetadata = { ...tableLine.metadata, cols: tableLine.cols + 1, columnWidths: normalizedWidths };
@@ -4078,7 +4520,7 @@ exports.aceInitialized = (h, ctx) => {
4078
4520
  // Special handling for deleting the first row (row index 0)
4079
4521
  // Insert a blank line to prevent the table from getting stuck at line 1
4080
4522
  if (targetRowIndex === 0) {
4081
- // log('[ep_data_tables] Deleting first row (row 0) - inserting blank line to prevent table from getting stuck');
4523
+ // log('[ep_data_tables] Deleting first row (row 0) - inserting blank line to prevent table from getting stuck');
4082
4524
  const insertStart = [targetLine.lineIndex, 0];
4083
4525
  editorInfo.ace_performDocumentReplaceRange(insertStart, insertStart, '\n');
4084
4526
 
@@ -4118,7 +4560,7 @@ exports.aceInitialized = (h, ctx) => {
4118
4560
  columnWidths.push(100 / targetLine.metadata.cols);
4119
4561
  }
4120
4562
  });
4121
- // log('[ep_data_tables] deleteTableRow: Extracted column widths from DOM:', columnWidths);
4563
+ // log('[ep_data_tables] deleteTableRow: Extracted column widths from DOM:', columnWidths);
4122
4564
  break;
4123
4565
  }
4124
4566
  }
@@ -4146,7 +4588,6 @@ exports.aceInitialized = (h, ctx) => {
4146
4588
  return false;
4147
4589
  }
4148
4590
  }
4149
-
4150
4591
  function deleteTableColumnWithText(tableLines, targetColIndex, editorInfo, docManager) {
4151
4592
  try {
4152
4593
  // Update text content for all table lines using precise character deletion
@@ -4155,7 +4596,7 @@ exports.aceInitialized = (h, ctx) => {
4155
4596
  const cells = lineText.split(DELIMITER);
4156
4597
 
4157
4598
  if (targetColIndex >= cells.length) {
4158
- // log(`[ep_data_tables] Warning: Target column ${targetColIndex} doesn't exist in line with ${cells.length} columns`);
4599
+ // log(`[ep_data_tables] Warning: Target column ${targetColIndex} doesn't exist in line with ${cells.length} columns`);
4159
4600
  continue;
4160
4601
  }
4161
4602
 
@@ -4180,7 +4621,7 @@ exports.aceInitialized = (h, ctx) => {
4180
4621
  deleteStart -= DELIMITER.length;
4181
4622
  }
4182
4623
 
4183
- // log(`[ep_data_tables] Deleting column ${targetColIndex} from line ${tableLine.lineIndex}: chars ${deleteStart}-${deleteEnd} from "${lineText}"`);
4624
+ // log(`[ep_data_tables] Deleting column ${targetColIndex} from line ${tableLine.lineIndex}: chars ${deleteStart}-${deleteEnd} from "${lineText}"`);
4184
4625
 
4185
4626
  // Perform the precise deletion
4186
4627
  const rangeStart = [tableLine.lineIndex, deleteStart];
@@ -4194,7 +4635,7 @@ exports.aceInitialized = (h, ctx) => {
4194
4635
  if (newColCount > 0) {
4195
4636
  const equalWidth = 100 / newColCount;
4196
4637
  const normalizedWidths = Array(newColCount).fill(equalWidth);
4197
- // log(`[ep_data_tables] deleteTableColumn: Reset all column widths to equal distribution: ${newColCount} columns at ${equalWidth.toFixed(1)}% each`);
4638
+ // log(`[ep_data_tables] deleteTableColumn: Reset all column widths to equal distribution: ${newColCount} columns at ${equalWidth.toFixed(1)}% each`);
4198
4639
 
4199
4640
  // Update metadata
4200
4641
  const newMetadata = { ...tableLine.metadata, cols: newColCount, columnWidths: normalizedWidths };
@@ -4212,7 +4653,7 @@ exports.aceInitialized = (h, ctx) => {
4212
4653
 
4213
4654
  // ... existing code ...
4214
4655
 
4215
- // log('aceInitialized: END - helpers defined.');
4656
+ // log('aceInitialized: END - helpers defined.');
4216
4657
  };
4217
4658
 
4218
4659
  // ───────────────────── required no‑op stubs ─────────────────────
@@ -4222,11 +4663,11 @@ exports.aceEndLineAndCharForPoint = () => { return undefined; };
4222
4663
  // NEW: Style protection for table cells
4223
4664
  exports.aceSetAuthorStyle = (hook, ctx) => {
4224
4665
  const logPrefix = '[ep_data_tables:aceSetAuthorStyle]';
4225
- // log(`${logPrefix} START`, { hook, ctx });
4666
+ // log(`${logPrefix} START`, { hook, ctx });
4226
4667
 
4227
4668
  // If no selection or no style to apply, allow default
4228
4669
  if (!ctx || !ctx.rep || !ctx.rep.selStart || !ctx.rep.selEnd || !ctx.key) {
4229
- // log(`${logPrefix} No selection or style key. Allowing default.`);
4670
+ // log(`${logPrefix} No selection or style key. Allowing default.`);
4230
4671
  return;
4231
4672
  }
4232
4673
 
@@ -4236,14 +4677,14 @@ exports.aceSetAuthorStyle = (hook, ctx) => {
4236
4677
 
4237
4678
  // If selection spans multiple lines, prevent style application
4238
4679
  if (startLine !== endLine) {
4239
- // log(`${logPrefix} Selection spans multiple lines. Preventing style application to protect table structure.`);
4680
+ // log(`${logPrefix} Selection spans multiple lines. Preventing style application to protect table structure.`);
4240
4681
  return false;
4241
4682
  }
4242
4683
 
4243
4684
  // Check if the line is a table line
4244
4685
  const lineAttrString = ctx.documentAttributeManager?.getAttributeOnLine(startLine, ATTR_TABLE_JSON);
4245
4686
  if (!lineAttrString) {
4246
- // log(`${logPrefix} Line ${startLine} is not a table line. Allowing default style application.`);
4687
+ // log(`${logPrefix} Line ${startLine} is not a table line. Allowing default style application.`);
4247
4688
  return;
4248
4689
  }
4249
4690
 
@@ -4254,7 +4695,7 @@ exports.aceSetAuthorStyle = (hook, ctx) => {
4254
4695
  ];
4255
4696
 
4256
4697
  if (BLOCKED_STYLES.includes(ctx.key)) {
4257
- // log(`${logPrefix} Blocked potentially harmful style '${ctx.key}' from being applied to table cell.`);
4698
+ // log(`${logPrefix} Blocked potentially harmful style '${ctx.key}' from being applied to table cell.`);
4258
4699
  return false;
4259
4700
  }
4260
4701
 
@@ -4262,7 +4703,7 @@ exports.aceSetAuthorStyle = (hook, ctx) => {
4262
4703
  try {
4263
4704
  const tableMetadata = JSON.parse(lineAttrString);
4264
4705
  if (!tableMetadata || typeof tableMetadata.cols !== 'number') {
4265
- // log(`${logPrefix} Invalid table metadata. Preventing style application.`);
4706
+ // log(`${logPrefix} Invalid table metadata. Preventing style application.`);
4266
4707
  return false;
4267
4708
  }
4268
4709
 
@@ -4288,7 +4729,7 @@ exports.aceSetAuthorStyle = (hook, ctx) => {
4288
4729
 
4289
4730
  // If selection spans multiple cells, prevent style application
4290
4731
  if (selectionStartCell !== selectionEndCell) {
4291
- // log(`${logPrefix} Selection spans multiple cells. Preventing style application to protect table structure.`);
4732
+ // log(`${logPrefix} Selection spans multiple cells. Preventing style application to protect table structure.`);
4292
4733
  return false;
4293
4734
  }
4294
4735
 
@@ -4297,15 +4738,15 @@ exports.aceSetAuthorStyle = (hook, ctx) => {
4297
4738
  const cellEndCol = cellStartCol + cells[selectionStartCell].length;
4298
4739
 
4299
4740
  if (ctx.rep.selStart[1] <= cellStartCol || ctx.rep.selEnd[1] >= cellEndCol) {
4300
- // log(`${logPrefix} Selection includes cell delimiters. Preventing style application to protect table structure.`);
4741
+ // log(`${logPrefix} Selection includes cell delimiters. Preventing style application to protect table structure.`);
4301
4742
  return false;
4302
4743
  }
4303
4744
 
4304
- // log(`${logPrefix} Style '${ctx.key}' allowed within cell boundaries.`);
4745
+ // log(`${logPrefix} Style '${ctx.key}' allowed within cell boundaries.`);
4305
4746
  return; // Allow the style to be applied
4306
4747
  } catch (e) {
4307
4748
  console.error(`${logPrefix} Error processing style application:`, e);
4308
- // log(`${logPrefix} Error details:`, { message: e.message, stack: e.stack });
4749
+ // log(`${logPrefix} Error details:`, { message: e.message, stack: e.stack });
4309
4750
  return false; // Prevent style application on error
4310
4751
  }
4311
4752
  };
@@ -4322,7 +4763,7 @@ exports.aceRegisterBlockElements = () => ['table'];
4322
4763
  // NEW: Column resize helper functions (adapted from images plugin)
4323
4764
  const startColumnResize = (table, columnIndex, startX, metadata, lineNum) => {
4324
4765
  const funcName = 'startColumnResize';
4325
- // log(`${funcName}: Starting resize for column ${columnIndex}`);
4766
+ // log(`${funcName}: Starting resize for column ${columnIndex}`);
4326
4767
 
4327
4768
  isResizing = true;
4328
4769
  resizeStartX = startX;
@@ -4336,7 +4777,7 @@ const startColumnResize = (table, columnIndex, startX, metadata, lineNum) => {
4336
4777
  const numCols = metadata.cols;
4337
4778
  resizeOriginalWidths = metadata.columnWidths ? [...metadata.columnWidths] : Array(numCols).fill(100 / numCols);
4338
4779
 
4339
- // log(`${funcName}: Original widths:`, resizeOriginalWidths);
4780
+ // log(`${funcName}: Original widths:`, resizeOriginalWidths);
4340
4781
 
4341
4782
  // Create visual overlay instead of modifying table directly
4342
4783
  createResizeOverlay(table, columnIndex);
@@ -4402,7 +4843,7 @@ const createResizeOverlay = (table, columnIndex) => {
4402
4843
 
4403
4844
  const totalTableHeight = maxBottom - minTop;
4404
4845
 
4405
- // log(`createResizeOverlay: Found ${allTableRows.length} table rows, total height: ${totalTableHeight}px`);
4846
+ // log(`createResizeOverlay: Found ${allTableRows.length} table rows, total height: ${totalTableHeight}px`);
4406
4847
 
4407
4848
  // Calculate positioning using the same method as image plugin
4408
4849
  let innerBodyRect, innerIframeRect, outerBodyRect;
@@ -4489,7 +4930,7 @@ const createResizeOverlay = (table, columnIndex) => {
4489
4930
  // Append to outer body like image plugin does with its outline
4490
4931
  padOuter.append(resizeOverlay);
4491
4932
 
4492
- // log('createResizeOverlay: Created Google Docs style blue line overlay spanning entire table height');
4933
+ // log('createResizeOverlay: Created Google Docs style blue line overlay spanning entire table height');
4493
4934
  };
4494
4935
 
4495
4936
  const updateColumnResize = (currentX) => {
@@ -4549,19 +4990,19 @@ const updateColumnResize = (currentX) => {
4549
4990
 
4550
4991
  const finishColumnResize = (editorInfo, docManager) => {
4551
4992
  if (!isResizing || !resizeTargetTable) {
4552
- // log('finishColumnResize: Not in resize mode');
4993
+ // log('finishColumnResize: Not in resize mode');
4553
4994
  return;
4554
4995
  }
4555
4996
 
4556
4997
  const funcName = 'finishColumnResize';
4557
- // log(`${funcName}: Finishing resize`);
4998
+ // log(`${funcName}: Finishing resize`);
4558
4999
 
4559
5000
  // Calculate final widths from actual mouse movement
4560
5001
  const tableRect = resizeTargetTable.getBoundingClientRect();
4561
5002
  const deltaX = resizeCurrentX - resizeStartX;
4562
5003
  const deltaPercent = (deltaX / tableRect.width) * 100;
4563
5004
 
4564
- // log(`${funcName}: Mouse moved ${deltaX}px (${deltaPercent.toFixed(1)}%)`);
5005
+ // log(`${funcName}: Mouse moved ${deltaX}px (${deltaPercent.toFixed(1)}%)`);
4565
5006
 
4566
5007
  const finalWidths = [...resizeOriginalWidths];
4567
5008
  const currentColumn = resizeTargetColumn;
@@ -4575,7 +5016,7 @@ const finishColumnResize = (editorInfo, docManager) => {
4575
5016
  finalWidths[currentColumn] += actualTransfer;
4576
5017
  finalWidths[nextColumn] -= actualTransfer;
4577
5018
 
4578
- // log(`${funcName}: Transferred ${actualTransfer.toFixed(1)}% from column ${nextColumn} to column ${currentColumn}`);
5019
+ // log(`${funcName}: Transferred ${actualTransfer.toFixed(1)}% from column ${nextColumn} to column ${currentColumn}`);
4579
5020
  }
4580
5021
 
4581
5022
  // Normalize widths
@@ -4586,7 +5027,7 @@ const finishColumnResize = (editorInfo, docManager) => {
4586
5027
  });
4587
5028
  }
4588
5029
 
4589
- // log(`${funcName}: Final normalized widths:`, finalWidths.map(w => w.toFixed(1) + '%'));
5030
+ // log(`${funcName}: Final normalized widths:`, finalWidths.map(w => w.toFixed(1) + '%'));
4590
5031
 
4591
5032
  // Clean up overlay
4592
5033
  if (resizeOverlay) {
@@ -4606,7 +5047,7 @@ const finishColumnResize = (editorInfo, docManager) => {
4606
5047
  // Apply updated metadata to ALL rows in the table (not just the resized row)
4607
5048
  editorInfo.ace_callWithAce((ace) => {
4608
5049
  const callWithAceLogPrefix = `${funcName}[ace_callWithAce]`;
4609
- // log(`${callWithAceLogPrefix}: Finding and updating all table rows with tblId: ${resizeTableMetadata.tblId}`);
5050
+ // log(`${callWithAceLogPrefix}: Finding and updating all table rows with tblId: ${resizeTableMetadata.tblId}`);
4610
5051
 
4611
5052
  try {
4612
5053
  const rep = ace.ace_getRep();
@@ -4663,7 +5104,7 @@ const finishColumnResize = (editorInfo, docManager) => {
4663
5104
  cols: domCells.length,
4664
5105
  columnWidths: columnWidths
4665
5106
  };
4666
- // log(`${callWithAceLogPrefix}: Reconstructed metadata from DOM for line ${lineIndex}:`, reconstructedMetadata);
5107
+ // log(`${callWithAceLogPrefix}: Reconstructed metadata from DOM for line ${lineIndex}:`, reconstructedMetadata);
4667
5108
  tableLines.push({
4668
5109
  lineIndex,
4669
5110
  metadata: reconstructedMetadata
@@ -4678,7 +5119,7 @@ const finishColumnResize = (editorInfo, docManager) => {
4678
5119
  }
4679
5120
  }
4680
5121
 
4681
- // log(`${callWithAceLogPrefix}: Found ${tableLines.length} table lines to update`);
5122
+ // log(`${callWithAceLogPrefix}: Found ${tableLines.length} table lines to update`);
4682
5123
 
4683
5124
  // Update all table lines with new column widths
4684
5125
  for (const tableLine of tableLines) {
@@ -4696,7 +5137,7 @@ const finishColumnResize = (editorInfo, docManager) => {
4696
5137
  const rangeStart = [tableLine.lineIndex, 0];
4697
5138
  const rangeEnd = [tableLine.lineIndex, lineLength];
4698
5139
 
4699
- // log(`${callWithAceLogPrefix}: Updating line ${tableLine.lineIndex} (row ${tableLine.metadata.row}) with new column widths`);
5140
+ // log(`${callWithAceLogPrefix}: Updating line ${tableLine.lineIndex} (row ${tableLine.metadata.row}) with new column widths`);
4700
5141
 
4701
5142
  // Apply the updated metadata attribute directly
4702
5143
  ace.ace_performDocumentApplyAttributesToRange(rangeStart, rangeEnd, [
@@ -4704,15 +5145,15 @@ const finishColumnResize = (editorInfo, docManager) => {
4704
5145
  ]);
4705
5146
  }
4706
5147
 
4707
- // log(`${callWithAceLogPrefix}: Successfully applied updated column widths to all ${tableLines.length} table rows`);
5148
+ // log(`${callWithAceLogPrefix}: Successfully applied updated column widths to all ${tableLines.length} table rows`);
4708
5149
 
4709
5150
  } catch (error) {
4710
5151
  console.error(`${callWithAceLogPrefix}: Error applying updated metadata:`, error);
4711
- // log(`${callWithAceLogPrefix}: Error details:`, { message: error.message, stack: error.stack });
5152
+ // log(`${callWithAceLogPrefix}: Error details:`, { message: error.message, stack: error.stack });
4712
5153
  }
4713
5154
  }, 'applyTableResizeToAllRows', true);
4714
5155
 
4715
- // log(`${funcName}: Column width update initiated for all table rows via ace_callWithAce`);
5156
+ // log(`${funcName}: Column width update initiated for all table rows via ace_callWithAce`);
4716
5157
 
4717
5158
  // Reset state
4718
5159
  resizeStartX = 0;
@@ -4723,16 +5164,16 @@ const finishColumnResize = (editorInfo, docManager) => {
4723
5164
  resizeTableMetadata = null;
4724
5165
  resizeLineNum = -1;
4725
5166
 
4726
- // log(`${funcName}: Resize complete - state reset`);
5167
+ // log(`${funcName}: Resize complete - state reset`);
4727
5168
  };
4728
5169
 
4729
5170
  // NEW: Undo/Redo protection
4730
5171
  exports.aceUndoRedo = (hook, ctx) => {
4731
5172
  const logPrefix = '[ep_data_tables:aceUndoRedo]';
4732
- // log(`${logPrefix} START`, { hook, ctx });
5173
+ // log(`${logPrefix} START`, { hook, ctx });
4733
5174
 
4734
5175
  if (!ctx || !ctx.rep || !ctx.rep.selStart || !ctx.rep.selEnd) {
4735
- // log(`${logPrefix} No selection or context. Allowing default.`);
5176
+ // log(`${logPrefix} No selection or context. Allowing default.`);
4736
5177
  return;
4737
5178
  }
4738
5179
 
@@ -4753,11 +5194,11 @@ exports.aceUndoRedo = (hook, ctx) => {
4753
5194
  }
4754
5195
 
4755
5196
  if (!hasTableLines) {
4756
- // log(`${logPrefix} No table lines affected. Allowing default undo/redo.`);
5197
+ // log(`${logPrefix} No table lines affected. Allowing default undo/redo.`);
4757
5198
  return;
4758
5199
  }
4759
5200
 
4760
- // log(`${logPrefix} Table lines affected:`, { tableLines });
5201
+ // log(`${logPrefix} Table lines affected:`, { tableLines });
4761
5202
 
4762
5203
  // Validate table structure after undo/redo
4763
5204
  try {
@@ -4767,7 +5208,7 @@ exports.aceUndoRedo = (hook, ctx) => {
4767
5208
 
4768
5209
  const tableMetadata = JSON.parse(lineAttrString);
4769
5210
  if (!tableMetadata || typeof tableMetadata.cols !== 'number') {
4770
- // log(`${logPrefix} Invalid table metadata after undo/redo. Attempting recovery.`);
5211
+ // log(`${logPrefix} Invalid table metadata after undo/redo. Attempting recovery.`);
4771
5212
  // Attempt to recover table structure
4772
5213
  const lineText = ctx.rep.lines.atIndex(line)?.text || '';
4773
5214
  const cells = lineText.split(DELIMITER);
@@ -4782,16 +5223,16 @@ exports.aceUndoRedo = (hook, ctx) => {
4782
5223
 
4783
5224
  // Apply the recovered metadata
4784
5225
  ctx.documentAttributeManager.setAttributeOnLine(line, ATTR_TABLE_JSON, JSON.stringify(newMetadata));
4785
- // log(`${logPrefix} Recovered table structure for line ${line}`);
5226
+ // log(`${logPrefix} Recovered table structure for line ${line}`);
4786
5227
  } else {
4787
5228
  // If we can't recover, remove the table attribute
4788
5229
  ctx.documentAttributeManager.removeAttributeOnLine(line, ATTR_TABLE_JSON);
4789
- // log(`${logPrefix} Removed invalid table attribute from line ${line}`);
5230
+ // log(`${logPrefix} Removed invalid table attribute from line ${line}`);
4790
5231
  }
4791
5232
  }
4792
5233
  }
4793
5234
  } catch (e) {
4794
5235
  console.error(`${logPrefix} Error during undo/redo validation:`, e);
4795
- // log(`${logPrefix} Error details:`, { message: e.message, stack: e.stack });
5236
+ // log(`${logPrefix} Error details:`, { message: e.message, stack: e.stack });
4796
5237
  }
4797
5238
  };