ep_data_tables 0.0.97 → 0.0.98
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.
- package/package.json +1 -1
- package/static/js/client_hooks.js +15 -801
package/package.json
CHANGED
|
@@ -741,807 +741,21 @@ exports.acePostWriteDomLineHTML = function (hook_name, args, cb) {
|
|
|
741
741
|
let skipMismatchReturn = false;
|
|
742
742
|
|
|
743
743
|
if (htmlSegments.length !== rowMetadata.cols) {
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
//
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
try {
|
|
760
|
-
if (!EP_DT_EDITOR_INFO) return;
|
|
761
|
-
const ed = EP_DT_EDITOR_INFO;
|
|
762
|
-
const docManager = ed.ep_data_tables_docManager || null;
|
|
763
|
-
|
|
764
|
-
if (__epDT_columnOperationInProgress) {
|
|
765
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] skipped - column operation in progress');
|
|
766
|
-
return;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
// Pre-check editor state (prevents crash if keyToNodeMap corrupted)
|
|
770
|
-
try {
|
|
771
|
-
const preCheckRep = ed.ace_getRep && ed.ace_getRep();
|
|
772
|
-
if (!preCheckRep || !preCheckRep.lines) {
|
|
773
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] skipped - rep unavailable');
|
|
774
|
-
return;
|
|
775
|
-
}
|
|
776
|
-
} catch (preCheckErr) {
|
|
777
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] skipped - pre-check failed', preCheckErr?.message);
|
|
778
|
-
return;
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
try {
|
|
782
|
-
ed.ace_callWithAce((ace) => {
|
|
783
|
-
try {
|
|
784
|
-
const rep = ace.ace_getRep();
|
|
785
|
-
if (!rep || !rep.lines) return;
|
|
786
|
-
|
|
787
|
-
const origLineInfo = __epDT_compositionOriginalLine;
|
|
788
|
-
const hasOriginalLineInfo = origLineInfo &&
|
|
789
|
-
origLineInfo.tblId === capturedTblId &&
|
|
790
|
-
typeof origLineInfo.lineNum === 'number' &&
|
|
791
|
-
(Date.now() - origLineInfo.timestamp) < 5000; // Only use if recent (within 5 seconds)
|
|
792
|
-
|
|
793
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] start', {
|
|
794
|
-
nodeId, fallbackLineNum, capturedTblId, capturedRow, expectedCols,
|
|
795
|
-
hasDocManager: !!docManager,
|
|
796
|
-
originalLineInfo: hasOriginalLineInfo ? origLineInfo.lineNum : null,
|
|
797
|
-
});
|
|
798
|
-
if (!docManager || typeof docManager.getAttributeOnLine !== 'function') {
|
|
799
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] abort: no documentAttributeManager available');
|
|
800
|
-
return;
|
|
801
|
-
}
|
|
802
|
-
// Robust line resolution: primary by attribute scan (tblId/row), then key lookup, then fallback.
|
|
803
|
-
let ln = -1;
|
|
804
|
-
let attrScanLine = -1; // Track where attr scan finds the table (may differ from original)
|
|
805
|
-
try {
|
|
806
|
-
const totalScan = rep.lines.length();
|
|
807
|
-
for (let i = 0; i < totalScan; i++) {
|
|
808
|
-
let sAttr = null;
|
|
809
|
-
try { sAttr = docManager.getAttributeOnLine(i, ATTR_TABLE_JSON); } catch (_) {}
|
|
810
|
-
if (!sAttr) continue;
|
|
811
|
-
try {
|
|
812
|
-
const m = JSON.parse(sAttr);
|
|
813
|
-
if (m && m.tblId === capturedTblId && m.row === capturedRow) {
|
|
814
|
-
attrScanLine = i;
|
|
815
|
-
ln = i;
|
|
816
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] found-by-attr-scan', { ln, tblId: m.tblId, row: m.row, cols: m.cols });
|
|
817
|
-
break;
|
|
818
|
-
}
|
|
819
|
-
} catch (_) { /* ignore parse errors */ }
|
|
820
|
-
}
|
|
821
|
-
} catch (scanErr) {
|
|
822
|
-
console.error('[ep_data_tables:postWriteCanonicalize] line scan error', scanErr);
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
if (hasOriginalLineInfo && attrScanLine >= 0 && attrScanLine !== origLineInfo.lineNum) {
|
|
826
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] line shift detected', {
|
|
827
|
-
attrScanLine,
|
|
828
|
-
originalLine: origLineInfo.lineNum,
|
|
829
|
-
shift: attrScanLine - origLineInfo.lineNum,
|
|
830
|
-
});
|
|
831
|
-
|
|
832
|
-
// Check if the original line still has a table DOM
|
|
833
|
-
const origEntry = rep.lines.atIndex(origLineInfo.lineNum);
|
|
834
|
-
if (origEntry?.lineNode) {
|
|
835
|
-
const origTableEl = origEntry.lineNode.querySelector(
|
|
836
|
-
`table.dataTable[data-tblId="${capturedTblId}"], table.dataTable[data-tblid="${capturedTblId}"]`
|
|
837
|
-
);
|
|
838
|
-
if (origTableEl) {
|
|
839
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] original line still has table DOM, preferring it', {
|
|
840
|
-
originalLine: origLineInfo.lineNum,
|
|
841
|
-
attrLine: attrScanLine,
|
|
842
|
-
});
|
|
843
|
-
ln = origLineInfo.lineNum;
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
if (ln < 0) {
|
|
849
|
-
try {
|
|
850
|
-
if (rep.lines && typeof rep.lines.indexOfKey === 'function') {
|
|
851
|
-
ln = rep.lines.indexOfKey(nodeId);
|
|
852
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] found-by-indexOfKey', { ln, nodeId });
|
|
853
|
-
}
|
|
854
|
-
} catch (eIdx) {
|
|
855
|
-
console.error('[ep_data_tables:postWriteCanonicalize] indexOfKey error; using fallback line', eIdx);
|
|
856
|
-
ln = (typeof fallbackLineNum === 'number') ? fallbackLineNum : -1;
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
if (typeof ln !== 'number' || ln < 0) {
|
|
860
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] abort: invalid line', { nodeId, ln });
|
|
861
|
-
return;
|
|
862
|
-
}
|
|
863
|
-
let attrStr = null;
|
|
864
|
-
try { attrStr = docManager.getAttributeOnLine(ln, ATTR_TABLE_JSON); } catch (_) { attrStr = null; }
|
|
865
|
-
if (!attrStr) {
|
|
866
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] abort: no tbljson on resolved line', { ln, nodeId, capturedTblId, capturedRow });
|
|
867
|
-
return;
|
|
868
|
-
}
|
|
869
|
-
let metaAttr = null;
|
|
870
|
-
try { metaAttr = JSON.parse(attrStr); } catch (_) { metaAttr = null; }
|
|
871
|
-
if (!metaAttr || metaAttr.tblId !== capturedTblId || metaAttr.row !== capturedRow) {
|
|
872
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] abort: meta mismatch', { ln, metaAttr, capturedTblId, capturedRow });
|
|
873
|
-
return;
|
|
874
|
-
}
|
|
875
|
-
if (typeof metaAttr.cols !== 'number') {
|
|
876
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] abort: invalid cols', { ln, metaAttr });
|
|
877
|
-
return;
|
|
878
|
-
}
|
|
879
|
-
const entry = rep.lines.atIndex(ln);
|
|
880
|
-
const currentText = entry?.text || '';
|
|
881
|
-
const segs = currentText.split(DELIMITER);
|
|
882
|
-
const needed = metaAttr.cols;
|
|
883
|
-
const sanitize = (s) => {
|
|
884
|
-
const x = normalizeSoftWhitespace((s || '').replace(new RegExp(DELIMITER, 'g'), ' ').replace(/[\u200B\u200C\u200D\uFEFF]/g, ' '));
|
|
885
|
-
return x || ' ';
|
|
886
|
-
};
|
|
887
|
-
const cells = new Array(needed);
|
|
888
|
-
for (let i = 0; i < needed; i++) cells[i] = sanitize(segs[i] || ' ');
|
|
889
|
-
const canonical = cells.join(DELIMITER);
|
|
890
|
-
|
|
891
|
-
// Check if line has table DOM (text matching alone is insufficient)
|
|
892
|
-
let lineHasTableDOM = false;
|
|
893
|
-
try {
|
|
894
|
-
const lineNode = entry?.lineNode;
|
|
895
|
-
if (lineNode && typeof lineNode.querySelector === 'function') {
|
|
896
|
-
const tableEl = lineNode.querySelector(
|
|
897
|
-
`table.dataTable[data-tblId="${capturedTblId}"], table.dataTable[data-tblid="${capturedTblId}"]`
|
|
898
|
-
);
|
|
899
|
-
lineHasTableDOM = !!tableEl;
|
|
900
|
-
}
|
|
901
|
-
} catch (_) {}
|
|
902
|
-
|
|
903
|
-
const textMatches = canonical === currentText;
|
|
904
|
-
const needsRepair = !textMatches || !lineHasTableDOM;
|
|
905
|
-
|
|
906
|
-
// Extract character-level styling before replacement
|
|
907
|
-
const extractedStyling = [];
|
|
908
|
-
try {
|
|
909
|
-
const lineNode = entry?.lineNode;
|
|
910
|
-
if (lineNode && lineHasTableDOM) {
|
|
911
|
-
const tableEl = lineNode.querySelector('table.dataTable');
|
|
912
|
-
if (tableEl) {
|
|
913
|
-
const tds = tableEl.querySelectorAll('td');
|
|
914
|
-
tds.forEach((td, cellIdx) => {
|
|
915
|
-
const spans = td.querySelectorAll('span:not(.ep-data_tables-delim):not(.ep-data_tables-caret-anchor)');
|
|
916
|
-
let relPos = 0; // Position relative to cell start
|
|
917
|
-
spans.forEach((span) => {
|
|
918
|
-
const text = (span.textContent || '').replace(/\u00A0/g, ' '); // Normalize nbsp
|
|
919
|
-
const textLen = text.length;
|
|
920
|
-
if (textLen === 0) return;
|
|
921
|
-
|
|
922
|
-
// Extract ALL classes except table-related ones
|
|
923
|
-
// Convert class names to Etherpad attribute format
|
|
924
|
-
const stylingAttrs = [];
|
|
925
|
-
if (span.classList) {
|
|
926
|
-
for (const cls of span.classList) {
|
|
927
|
-
if (cls.startsWith('tbljson-') || cls.startsWith('tblCell-')) continue;
|
|
928
|
-
if (cls === 'ace-line' || cls.startsWith('ep-data_tables-')) continue;
|
|
929
|
-
|
|
930
|
-
// Parse class name to attribute key-value pair
|
|
931
|
-
// Etherpad uses formats like: "author-xyz", "font-size:12", "bold"
|
|
932
|
-
if (cls.includes(':')) {
|
|
933
|
-
// Format: "key:value" (e.g., "font-size:12", "color:red")
|
|
934
|
-
const colonIdx = cls.indexOf(':');
|
|
935
|
-
const key = cls.substring(0, colonIdx);
|
|
936
|
-
const value = cls.substring(colonIdx + 1);
|
|
937
|
-
stylingAttrs.push([key, value]);
|
|
938
|
-
} else if (cls.includes('-')) {
|
|
939
|
-
// Format: "key-value" (e.g., "author-a1b2c3")
|
|
940
|
-
const dashIdx = cls.indexOf('-');
|
|
941
|
-
const key = cls.substring(0, dashIdx);
|
|
942
|
-
const value = cls.substring(dashIdx + 1);
|
|
943
|
-
stylingAttrs.push([key, value]);
|
|
944
|
-
} else {
|
|
945
|
-
// Format: "key" only (e.g., "bold", "italic")
|
|
946
|
-
// These are boolean attributes set to "true"
|
|
947
|
-
stylingAttrs.push([cls, 'true']);
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
if (stylingAttrs.length > 0) {
|
|
953
|
-
extractedStyling.push({
|
|
954
|
-
cellIdx,
|
|
955
|
-
relStart: relPos,
|
|
956
|
-
len: textLen,
|
|
957
|
-
text: text, // Store text for matching
|
|
958
|
-
attrs: stylingAttrs,
|
|
959
|
-
});
|
|
960
|
-
}
|
|
961
|
-
relPos += textLen;
|
|
962
|
-
});
|
|
963
|
-
});
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
if (extractedStyling.length > 0) {
|
|
967
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] extracted styling', {
|
|
968
|
-
ln, count: extractedStyling.length,
|
|
969
|
-
sample: extractedStyling.slice(0, 2).map(s => ({ cell: s.cellIdx, attrs: s.attrs })),
|
|
970
|
-
});
|
|
971
|
-
}
|
|
972
|
-
} catch (extractErr) {
|
|
973
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] styling extraction error (non-fatal)', extractErr?.message);
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
if (needsRepair) {
|
|
977
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] line needs repair', {
|
|
978
|
-
ln,
|
|
979
|
-
textMatches,
|
|
980
|
-
lineHasTableDOM,
|
|
981
|
-
fromLen: currentText.length,
|
|
982
|
-
toLen: canonical.length,
|
|
983
|
-
});
|
|
984
|
-
if (!textMatches) {
|
|
985
|
-
// Validate line is safe to modify before replacement (prevents keyToNodeMap errors)
|
|
986
|
-
const repBeforeRepair = ace.ace_getRep();
|
|
987
|
-
if (!isLineSafeToModify(repBeforeRepair, ln, '[ep_data_tables:postWriteCanonicalize] repair')) {
|
|
988
|
-
console.warn('[ep_data_tables:postWriteCanonicalize] line not safe to modify, skipping repair');
|
|
989
|
-
return cb();
|
|
990
|
-
}
|
|
991
|
-
ace.ace_performDocumentReplaceRange([ln, 0], [ln, currentText.length], canonical);
|
|
992
|
-
}
|
|
993
|
-
// Note: If lineHasTableDOM is false, orphan detection below will handle finding the real table
|
|
994
|
-
} else {
|
|
995
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] line already canonical', { ln, lineHasTableDOM });
|
|
996
|
-
}
|
|
997
|
-
let offset = 0;
|
|
998
|
-
for (let i = 0; i < cells.length; i++) {
|
|
999
|
-
const len = cells[i].length;
|
|
1000
|
-
if (len > 0) {
|
|
1001
|
-
ace.ace_performDocumentApplyAttributesToRange([ln, offset], [ln, offset + len], [[ATTR_CELL, String(i)]]);
|
|
1002
|
-
}
|
|
1003
|
-
offset += len;
|
|
1004
|
-
if (i < cells.length - 1) offset += DELIMITER.length;
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
if (extractedStyling.length > 0) {
|
|
1008
|
-
let appliedCount = 0;
|
|
1009
|
-
try {
|
|
1010
|
-
for (const style of extractedStyling) {
|
|
1011
|
-
const cellIdx = style.cellIdx;
|
|
1012
|
-
const cellContent = cells[cellIdx] || '';
|
|
1013
|
-
const styledText = style.text;
|
|
1014
|
-
|
|
1015
|
-
if (!styledText || styledText.length === 0 || !cellContent) continue;
|
|
1016
|
-
|
|
1017
|
-
// Find where the styled text appears in the NEW cell content
|
|
1018
|
-
let foundPos = cellContent.indexOf(styledText);
|
|
1019
|
-
|
|
1020
|
-
if (foundPos === -1) {
|
|
1021
|
-
const lowerCell = cellContent.toLowerCase();
|
|
1022
|
-
const lowerStyled = styledText.toLowerCase();
|
|
1023
|
-
foundPos = lowerCell.indexOf(lowerStyled);
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
if (foundPos === -1) continue; // Text not found in cell
|
|
1027
|
-
|
|
1028
|
-
// Calculate absolute position in the line
|
|
1029
|
-
let absStart = 0;
|
|
1030
|
-
for (let c = 0; c < cellIdx; c++) {
|
|
1031
|
-
absStart += (cells[c]?.length || 0) + DELIMITER.length;
|
|
1032
|
-
}
|
|
1033
|
-
absStart += foundPos;
|
|
1034
|
-
const absEnd = absStart + styledText.length;
|
|
1035
|
-
|
|
1036
|
-
if (style.attrs.length > 0) {
|
|
1037
|
-
ace.ace_performDocumentApplyAttributesToRange(
|
|
1038
|
-
[ln, absStart],
|
|
1039
|
-
[ln, absEnd],
|
|
1040
|
-
style.attrs
|
|
1041
|
-
);
|
|
1042
|
-
appliedCount++;
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
if (appliedCount > 0) {
|
|
1046
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] re-applied styling', {
|
|
1047
|
-
ln, applied: appliedCount, total: extractedStyling.length,
|
|
1048
|
-
});
|
|
1049
|
-
}
|
|
1050
|
-
} catch (applyErr) {
|
|
1051
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] styling re-apply error (non-fatal)', applyErr?.message);
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
try {
|
|
1056
|
-
ed.ep_data_tables_applyMeta(ln, metaAttr.tblId, metaAttr.row, metaAttr.cols, ace.ace_getRep(), ed, JSON.stringify(metaAttr), docManager);
|
|
1057
|
-
} catch (metaErr) {
|
|
1058
|
-
console.error('[ep_data_tables:postWriteCanonicalize] meta apply error', metaErr);
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
if (!lineHasTableDOM) {
|
|
1062
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] skipping orphan detection (no table on primary line)', { ln, capturedTblId, capturedRow });
|
|
1063
|
-
return;
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
// Detect orphan lines (same tblId/row or tbljson-* spans without table)
|
|
1067
|
-
const orphanLines = [];
|
|
1068
|
-
const seenOrphanLines = new Set();
|
|
1069
|
-
try {
|
|
1070
|
-
const total = rep.lines.length();
|
|
1071
|
-
for (let li = 0; li < total; li++) {
|
|
1072
|
-
if (li === ln) continue;
|
|
1073
|
-
|
|
1074
|
-
// Check line attribute
|
|
1075
|
-
let sOther = null;
|
|
1076
|
-
try { sOther = docManager.getAttributeOnLine(li, ATTR_TABLE_JSON); } catch (_) { sOther = null; }
|
|
1077
|
-
if (sOther) {
|
|
1078
|
-
let mOther = null;
|
|
1079
|
-
try { mOther = JSON.parse(sOther); } catch (_) { mOther = null; }
|
|
1080
|
-
if (mOther && mOther.tblId === capturedTblId && mOther.row === capturedRow) {
|
|
1081
|
-
const orphanEntry = rep.lines.atIndex(li);
|
|
1082
|
-
const orphanText = orphanEntry?.text || '';
|
|
1083
|
-
orphanLines.push({ lineNum: li, text: orphanText, meta: mOther, source: 'attr' });
|
|
1084
|
-
seenOrphanLines.add(li);
|
|
1085
|
-
continue;
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
// DOM-based detection (tbljson-* or tblCell-* spans without table parent)
|
|
1090
|
-
if (seenOrphanLines.has(li)) continue;
|
|
1091
|
-
try {
|
|
1092
|
-
const lineEntry = rep.lines.atIndex(li);
|
|
1093
|
-
const lineNode = lineEntry?.lineNode;
|
|
1094
|
-
if (lineNode) {
|
|
1095
|
-
const hasTable = lineNode.querySelector('table.dataTable');
|
|
1096
|
-
if (!hasTable) {
|
|
1097
|
-
const tbljsonSpan = lineNode.querySelector('[class*="tbljson-"]');
|
|
1098
|
-
if (tbljsonSpan) {
|
|
1099
|
-
for (const cls of tbljsonSpan.classList) {
|
|
1100
|
-
if (cls.startsWith('tbljson-')) {
|
|
1101
|
-
try {
|
|
1102
|
-
const decoded = JSON.parse(atob(cls.substring(8)));
|
|
1103
|
-
if (decoded && decoded.tblId === capturedTblId && decoded.row === capturedRow) {
|
|
1104
|
-
const orphanText = lineEntry?.text || '';
|
|
1105
|
-
orphanLines.push({
|
|
1106
|
-
lineNum: li,
|
|
1107
|
-
text: orphanText,
|
|
1108
|
-
meta: decoded,
|
|
1109
|
-
source: 'dom-class-tbljson',
|
|
1110
|
-
});
|
|
1111
|
-
seenOrphanLines.add(li);
|
|
1112
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] detected tbljson DOM orphan', {
|
|
1113
|
-
lineNum: li, tblId: decoded.tblId, row: decoded.row, textLen: orphanText.length,
|
|
1114
|
-
});
|
|
1115
|
-
break;
|
|
1116
|
-
}
|
|
1117
|
-
} catch (_) {}
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
// Also check for tblCell-* spans (no encoded metadata)
|
|
1123
|
-
if (!seenOrphanLines.has(li)) {
|
|
1124
|
-
const tblCellSpan = lineNode.querySelector('[class*="tblCell-"]');
|
|
1125
|
-
if (tblCellSpan) {
|
|
1126
|
-
// SAFETY CHECK: Verify this line has OUR tbljson attribute, just row might differ
|
|
1127
|
-
let belongsToOurTable = false;
|
|
1128
|
-
try {
|
|
1129
|
-
const lineAttr = docManager.getAttributeOnLine(li, ATTR_TABLE_JSON);
|
|
1130
|
-
if (lineAttr) {
|
|
1131
|
-
const lineMeta = JSON.parse(lineAttr);
|
|
1132
|
-
if (lineMeta && lineMeta.tblId === capturedTblId) {
|
|
1133
|
-
belongsToOurTable = true;
|
|
1134
|
-
}
|
|
1135
|
-
} else {
|
|
1136
|
-
// No tbljson attribute - check if there's a tbljson-* CLASS that we can decode
|
|
1137
|
-
const anyTbljsonSpan = lineNode.querySelector('[class*="tbljson-"]');
|
|
1138
|
-
if (anyTbljsonSpan) {
|
|
1139
|
-
for (const cls of anyTbljsonSpan.classList) {
|
|
1140
|
-
if (cls.startsWith('tbljson-')) {
|
|
1141
|
-
try {
|
|
1142
|
-
const decoded = JSON.parse(atob(cls.substring(8)));
|
|
1143
|
-
if (decoded && decoded.tblId === capturedTblId) {
|
|
1144
|
-
belongsToOurTable = true;
|
|
1145
|
-
}
|
|
1146
|
-
} catch (_) {}
|
|
1147
|
-
break;
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
} catch (_) {}
|
|
1153
|
-
|
|
1154
|
-
if (belongsToOurTable) {
|
|
1155
|
-
const orphanText = lineEntry?.text || '';
|
|
1156
|
-
if (orphanText.includes(DELIMITER) || orphanText.trim().length > 0) {
|
|
1157
|
-
orphanLines.push({
|
|
1158
|
-
lineNum: li,
|
|
1159
|
-
text: orphanText,
|
|
1160
|
-
meta: { tblId: capturedTblId, row: capturedRow, cols: expectedCols },
|
|
1161
|
-
source: 'dom-class-tblCell',
|
|
1162
|
-
});
|
|
1163
|
-
seenOrphanLines.add(li);
|
|
1164
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] detected tblCell DOM orphan', {
|
|
1165
|
-
lineNum: li, capturedTblId, capturedRow, textLen: orphanText.length,
|
|
1166
|
-
});
|
|
1167
|
-
}
|
|
1168
|
-
} else {
|
|
1169
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] skipping tblCell line (different table)', {
|
|
1170
|
-
lineNum: li, capturedTblId,
|
|
1171
|
-
});
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
} catch (domErr) {
|
|
1178
|
-
console.error('[ep_data_tables:postWriteCanonicalize] DOM orphan detection error', domErr);
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
} catch (scanOrphanErr) {
|
|
1182
|
-
console.error('[ep_data_tables:postWriteCanonicalize] orphan scan error', scanOrphanErr);
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
if (orphanLines.length > 0) {
|
|
1186
|
-
let mainEntry = rep.lines.atIndex(ln);
|
|
1187
|
-
|
|
1188
|
-
let lineWithTableIdx = -1;
|
|
1189
|
-
try {
|
|
1190
|
-
const innerDoc = node?.ownerDocument || document;
|
|
1191
|
-
const editorBody = innerDoc.getElementById('innerdocbody') || innerDoc.body;
|
|
1192
|
-
if (editorBody) {
|
|
1193
|
-
const allAceLines = editorBody.querySelectorAll('div.ace-line');
|
|
1194
|
-
for (let ai = 0; ai < allAceLines.length; ai++) {
|
|
1195
|
-
const aceLine = allAceLines[ai];
|
|
1196
|
-
const tableEl = aceLine.querySelector(
|
|
1197
|
-
`table.dataTable[data-tblId="${capturedTblId}"][data-row="${capturedRow}"], ` +
|
|
1198
|
-
`table.dataTable[data-tblid="${capturedTblId}"][data-row="${capturedRow}"]`
|
|
1199
|
-
);
|
|
1200
|
-
if (tableEl) {
|
|
1201
|
-
lineWithTableIdx = ai;
|
|
1202
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] LIVE DOM found table', {
|
|
1203
|
-
domIndex: ai, aceLineId: aceLine.id, tblId: capturedTblId, row: capturedRow,
|
|
1204
|
-
});
|
|
1205
|
-
break;
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
} catch (domQueryErr) {
|
|
1210
|
-
console.error('[ep_data_tables:postWriteCanonicalize] live DOM query error', domQueryErr);
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
if (lineWithTableIdx >= 0 && lineWithTableIdx !== ln) {
|
|
1214
|
-
// Find if this line is in our orphan list
|
|
1215
|
-
const orphanIdx = orphanLines.findIndex(o => o.lineNum === lineWithTableIdx);
|
|
1216
|
-
if (orphanIdx >= 0) {
|
|
1217
|
-
console.warn('[ep_data_tables:postWriteCanonicalize] SWAPPING primary via LIVE DOM: table found on orphan line', {
|
|
1218
|
-
oldPrimary: ln,
|
|
1219
|
-
newPrimary: lineWithTableIdx,
|
|
1220
|
-
orphanIdx,
|
|
1221
|
-
});
|
|
1222
|
-
|
|
1223
|
-
// Move current "primary" to orphan list
|
|
1224
|
-
const formerPrimary = {
|
|
1225
|
-
lineNum: ln,
|
|
1226
|
-
text: mainEntry?.text || '',
|
|
1227
|
-
meta: metaAttr,
|
|
1228
|
-
source: 'swapped-to-orphan',
|
|
1229
|
-
};
|
|
1230
|
-
|
|
1231
|
-
// Swap: orphan becomes primary
|
|
1232
|
-
const newPrimary = orphanLines[orphanIdx];
|
|
1233
|
-
ln = newPrimary.lineNum;
|
|
1234
|
-
metaAttr = newPrimary.meta;
|
|
1235
|
-
mainEntry = rep.lines.atIndex(ln);
|
|
1236
|
-
|
|
1237
|
-
orphanLines.splice(orphanIdx, 1);
|
|
1238
|
-
orphanLines.push(formerPrimary);
|
|
1239
|
-
|
|
1240
|
-
const newText = mainEntry?.text || '';
|
|
1241
|
-
const newSegs = newText.split(DELIMITER);
|
|
1242
|
-
for (let i = 0; i < needed; i++) {
|
|
1243
|
-
cells[i] = sanitize(newSegs[i] || ' ');
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
} else if (lineWithTableIdx < 0) {
|
|
1247
|
-
// No line has the table - this is bad, but we should still try to preserve content
|
|
1248
|
-
console.warn('[ep_data_tables:postWriteCanonicalize] WARNING: no line has table DOM for tblId/row', {
|
|
1249
|
-
capturedTblId, capturedRow, primaryLn: ln, orphanCount: orphanLines.length,
|
|
1250
|
-
});
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
console.warn('[ep_data_tables:postWriteCanonicalize] orphan lines detected - merging content', {
|
|
1254
|
-
keepLine: ln, orphans: orphanLines.map(o => ({ line: o.lineNum, textLen: o.text.length })),
|
|
1255
|
-
tableFoundOnLine: lineWithTableIdx,
|
|
1256
|
-
});
|
|
1257
|
-
|
|
1258
|
-
// CRITICAL FIX: Extract cell content from LIVE DOM table, not from line text!
|
|
1259
|
-
const mainText = mainEntry?.text || '';
|
|
1260
|
-
const mainSegs = mainText.split(DELIMITER);
|
|
1261
|
-
const mergedCells = new Array(needed);
|
|
1262
|
-
|
|
1263
|
-
// Try to get clean cell content from the DOM table
|
|
1264
|
-
let usedDomContent = false;
|
|
1265
|
-
if (lineWithTableIdx >= 0) {
|
|
1266
|
-
try {
|
|
1267
|
-
const innerDoc = node?.ownerDocument || document;
|
|
1268
|
-
const editorBody = innerDoc.getElementById('innerdocbody') || innerDoc.body;
|
|
1269
|
-
const allAceLines = editorBody?.querySelectorAll('div.ace-line');
|
|
1270
|
-
const tableAceLine = allAceLines?.[lineWithTableIdx];
|
|
1271
|
-
const tableEl = tableAceLine?.querySelector(
|
|
1272
|
-
`table.dataTable[data-tblId="${capturedTblId}"], table.dataTable[data-tblid="${capturedTblId}"]`
|
|
1273
|
-
);
|
|
1274
|
-
if (tableEl) {
|
|
1275
|
-
const tr = tableEl.querySelector('tbody > tr');
|
|
1276
|
-
if (tr && tr.children.length === needed) {
|
|
1277
|
-
for (let i = 0; i < needed; i++) {
|
|
1278
|
-
const td = tr.children[i];
|
|
1279
|
-
// Extract text content, excluding delimiters and special elements
|
|
1280
|
-
let cellText = '';
|
|
1281
|
-
for (const child of td.childNodes) {
|
|
1282
|
-
if (child.nodeType === 3) { // Text node
|
|
1283
|
-
cellText += child.textContent || '';
|
|
1284
|
-
} else if (child.nodeType === 1) { // Element
|
|
1285
|
-
const el = child;
|
|
1286
|
-
if (el.classList?.contains('ep-data_tables-delim')) continue;
|
|
1287
|
-
if (el.classList?.contains('ep-data_tables-caret-anchor')) continue;
|
|
1288
|
-
if (el.classList?.contains('ep-data_tables-resize-handle')) continue;
|
|
1289
|
-
cellText += el.textContent || '';
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
mergedCells[i] = sanitize(cellText || ' ');
|
|
1293
|
-
}
|
|
1294
|
-
usedDomContent = true;
|
|
1295
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] using DOM table content for merge base', {
|
|
1296
|
-
cells: mergedCells.map(c => c.slice(0, 20)),
|
|
1297
|
-
});
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
} catch (domExtractErr) {
|
|
1301
|
-
console.error('[ep_data_tables:postWriteCanonicalize] DOM content extraction error', domExtractErr);
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
if (!usedDomContent) {
|
|
1306
|
-
for (let i = 0; i < needed; i++) {
|
|
1307
|
-
mergedCells[i] = sanitize(mainSegs[i] || ' ');
|
|
1308
|
-
}
|
|
1309
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] using line text for merge base (DOM extraction failed)');
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
for (const orphan of orphanLines) {
|
|
1313
|
-
const orphanSegs = orphan.text.split(DELIMITER);
|
|
1314
|
-
for (let i = 0; i < Math.min(orphanSegs.length, needed); i++) {
|
|
1315
|
-
const orphanContent = sanitize(orphanSegs[i] || '');
|
|
1316
|
-
if (!orphanContent || orphanContent.trim() === '') continue;
|
|
1317
|
-
const mainCellTrimmed = (mergedCells[i] || '').trim();
|
|
1318
|
-
const orphanTrimmed = orphanContent.trim();
|
|
1319
|
-
if (mainCellTrimmed.includes(orphanTrimmed)) continue;
|
|
1320
|
-
if (!mainCellTrimmed) {
|
|
1321
|
-
mergedCells[i] = orphanContent;
|
|
1322
|
-
} else {
|
|
1323
|
-
// Append orphan content to existing cell content (preserve user data)
|
|
1324
|
-
mergedCells[i] = mainCellTrimmed + orphanTrimmed;
|
|
1325
|
-
}
|
|
1326
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] merged orphan content', {
|
|
1327
|
-
cellIdx: i, orphanLine: orphan.lineNum, orphanContent, mergedResult: mergedCells[i],
|
|
1328
|
-
});
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
const repBeforeMerge = ace.ace_getRep();
|
|
1333
|
-
if (!isLineSafeToModify(repBeforeMerge, ln, '[ep_data_tables:postWriteCanonicalize] merge-target')) {
|
|
1334
|
-
console.warn('[ep_data_tables:postWriteCanonicalize] main line not safe to modify, aborting merge');
|
|
1335
|
-
} else {
|
|
1336
|
-
const mergedCanonical = mergedCells.join(DELIMITER);
|
|
1337
|
-
if (mergedCanonical !== mainText) {
|
|
1338
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] applying merged canonical line', {
|
|
1339
|
-
ln, fromLen: mainText.length, toLen: mergedCanonical.length, mergedCells,
|
|
1340
|
-
});
|
|
1341
|
-
ace.ace_performDocumentReplaceRange([ln, 0], [ln, mainText.length], mergedCanonical);
|
|
1342
|
-
|
|
1343
|
-
let mergeOffset = 0;
|
|
1344
|
-
for (let i = 0; i < mergedCells.length; i++) {
|
|
1345
|
-
const cellLen = mergedCells[i].length;
|
|
1346
|
-
if (cellLen > 0) {
|
|
1347
|
-
ace.ace_performDocumentApplyAttributesToRange([ln, mergeOffset], [ln, mergeOffset + cellLen], [[ATTR_CELL, String(i)]]);
|
|
1348
|
-
}
|
|
1349
|
-
mergeOffset += cellLen;
|
|
1350
|
-
if (i < mergedCells.length - 1) mergeOffset += DELIMITER.length;
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
try {
|
|
1354
|
-
const repAfterMerge = ace.ace_getRep();
|
|
1355
|
-
ed.ep_data_tables_applyMeta(ln, capturedTblId, capturedRow, expectedCols, repAfterMerge, ed, JSON.stringify(metaAttr), docManager);
|
|
1356
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] re-applied tbljson after merge', {
|
|
1357
|
-
ln, tblId: capturedTblId, row: capturedRow, cols: expectedCols,
|
|
1358
|
-
});
|
|
1359
|
-
} catch (metaMergeErr) {
|
|
1360
|
-
console.error('[ep_data_tables:postWriteCanonicalize] failed to re-apply tbljson after merge', metaMergeErr);
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
} // end isLineSafeToModify check for merge target
|
|
1364
|
-
|
|
1365
|
-
// Delete orphan lines bottom-up
|
|
1366
|
-
// Validate lines and check DOM desync before destructive ops
|
|
1367
|
-
if (!isDestructiveOperationSafe('postWriteCanonicalize orphan removal')) {
|
|
1368
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] skipping orphan removal (safe mode)');
|
|
1369
|
-
} else {
|
|
1370
|
-
orphanLines.sort((a, b) => b.lineNum - a.lineNum).forEach((orphan) => {
|
|
1371
|
-
try {
|
|
1372
|
-
// Re-fetch rep to get current state after any previous deletions
|
|
1373
|
-
const repCheck = ace.ace_getRep();
|
|
1374
|
-
|
|
1375
|
-
if (!isLineSafeToModify(repCheck, orphan.lineNum, '[ep_data_tables:postWriteCanonicalize] orphan')) {
|
|
1376
|
-
return;
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
try {
|
|
1380
|
-
if (docManager && typeof docManager.removeAttributeOnLine === 'function') {
|
|
1381
|
-
docManager.removeAttributeOnLine(orphan.lineNum, ATTR_TABLE_JSON);
|
|
1382
|
-
}
|
|
1383
|
-
} catch (remErr) {
|
|
1384
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] removeAttributeOnLine error (non-fatal)', remErr?.message);
|
|
1385
|
-
}
|
|
1386
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] removing orphan line (content already merged)', {
|
|
1387
|
-
orphanLine: orphan.lineNum,
|
|
1388
|
-
});
|
|
1389
|
-
ace.ace_performDocumentReplaceRange([orphan.lineNum, 0], [orphan.lineNum + 1, 0], '');
|
|
1390
|
-
} catch (orphanRemErr) {
|
|
1391
|
-
console.error('[ep_data_tables:postWriteCanonicalize] orphan line removal error', {
|
|
1392
|
-
orphanLine: orphan.lineNum,
|
|
1393
|
-
error: orphanRemErr?.message || orphanRemErr,
|
|
1394
|
-
});
|
|
1395
|
-
// Check if this was a desync error
|
|
1396
|
-
handleDomDesyncError(orphanRemErr, 'postWriteCanonicalize orphan removal');
|
|
1397
|
-
}
|
|
1398
|
-
});
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
// Clean up spurious blank lines between this table row and the next
|
|
1402
|
-
// These can be created when orphan content gets merged and lines shift
|
|
1403
|
-
try {
|
|
1404
|
-
const repAfterOrphanRemoval = ace.ace_getRep();
|
|
1405
|
-
const currentLineNum = ln;
|
|
1406
|
-
const nextRowNum = capturedRow + 1;
|
|
1407
|
-
|
|
1408
|
-
let nextRowLineNum = -1;
|
|
1409
|
-
const totalAfter = repAfterOrphanRemoval.lines.length();
|
|
1410
|
-
for (let li = currentLineNum + 1; li < totalAfter && li < currentLineNum + 10; li++) {
|
|
1411
|
-
try {
|
|
1412
|
-
const attrStr = docManager.getAttributeOnLine(li, ATTR_TABLE_JSON);
|
|
1413
|
-
if (attrStr) {
|
|
1414
|
-
const meta = JSON.parse(attrStr);
|
|
1415
|
-
if (meta && meta.tblId === capturedTblId && meta.row === nextRowNum) {
|
|
1416
|
-
nextRowLineNum = li;
|
|
1417
|
-
break;
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
} catch (_) {}
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
// If there are blank lines between current row and next row, remove them
|
|
1424
|
-
if (nextRowLineNum > currentLineNum + 1) {
|
|
1425
|
-
const blankLinesToRemove = [];
|
|
1426
|
-
for (let li = currentLineNum + 1; li < nextRowLineNum; li++) {
|
|
1427
|
-
const lineEntry = repAfterOrphanRemoval.lines.atIndex(li);
|
|
1428
|
-
const lineText = lineEntry?.text || '';
|
|
1429
|
-
// Check if line is blank (empty or just whitespace)
|
|
1430
|
-
if (!lineText.trim() || lineText === '\n') {
|
|
1431
|
-
blankLinesToRemove.push(li);
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
// Remove blank lines bottom-up to preserve line numbers
|
|
1436
|
-
if (!isDestructiveOperationSafe('postWriteCanonicalize blank line removal')) {
|
|
1437
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] skipping blank line removal (safe mode)');
|
|
1438
|
-
} else {
|
|
1439
|
-
blankLinesToRemove.sort((a, b) => b - a).forEach((blankLineNum) => {
|
|
1440
|
-
try {
|
|
1441
|
-
// Re-fetch rep and validate line before removal (prevents keyToNodeMap errors)
|
|
1442
|
-
const repBlankCheck = ace.ace_getRep();
|
|
1443
|
-
if (!isLineSafeToModify(repBlankCheck, blankLineNum, '[ep_data_tables:postWriteCanonicalize] blank')) {
|
|
1444
|
-
return;
|
|
1445
|
-
}
|
|
1446
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] removing spurious blank line between table rows', {
|
|
1447
|
-
blankLineNum, betweenRows: [capturedRow, nextRowNum],
|
|
1448
|
-
});
|
|
1449
|
-
ace.ace_performDocumentReplaceRange([blankLineNum, 0], [blankLineNum + 1, 0], '');
|
|
1450
|
-
} catch (blankRemErr) {
|
|
1451
|
-
console.error('[ep_data_tables:postWriteCanonicalize] blank line removal error', blankRemErr);
|
|
1452
|
-
handleDomDesyncError(blankRemErr, 'postWriteCanonicalize blank removal');
|
|
1453
|
-
}
|
|
1454
|
-
});
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
} catch (cleanupErr) {
|
|
1458
|
-
console.error('[ep_data_tables:postWriteCanonicalize] blank line cleanup error', cleanupErr);
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
try { ace.ace_fastIncorp(5); } catch (_) {}
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
// ALWAYS run blank line cleanup after repair, even if there were no orphans
|
|
1465
|
-
// Grammarly and other extensions can create blank lines without triggering orphan detection
|
|
1466
|
-
try {
|
|
1467
|
-
const repFinal = ace.ace_getRep();
|
|
1468
|
-
if (repFinal && repFinal.lines) {
|
|
1469
|
-
const finalLineNum = ln;
|
|
1470
|
-
const totalFinal = repFinal.lines.length();
|
|
1471
|
-
|
|
1472
|
-
const blankLinesToClean = [];
|
|
1473
|
-
for (let li = finalLineNum + 1; li < totalFinal && li < finalLineNum + 5; li++) {
|
|
1474
|
-
const lineEntry = repFinal.lines.atIndex(li);
|
|
1475
|
-
const lineText = lineEntry?.text || '';
|
|
1476
|
-
|
|
1477
|
-
// Check if this is a table row (should stop scanning)
|
|
1478
|
-
let isTableRow = false;
|
|
1479
|
-
try {
|
|
1480
|
-
const attrStr = docManager.getAttributeOnLine(li, ATTR_TABLE_JSON);
|
|
1481
|
-
if (attrStr) isTableRow = true;
|
|
1482
|
-
} catch (_) {}
|
|
1483
|
-
|
|
1484
|
-
if (isTableRow) break; // Stop at next table row
|
|
1485
|
-
|
|
1486
|
-
// Check if blank line (empty or just whitespace)
|
|
1487
|
-
if (!lineText.trim() || lineText === '\n') {
|
|
1488
|
-
blankLinesToClean.push(li);
|
|
1489
|
-
} else {
|
|
1490
|
-
break; // Stop at first non-blank, non-table line
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
// Remove blank lines bottom-up
|
|
1495
|
-
// Check for DOM desync before destructive operations
|
|
1496
|
-
if (blankLinesToClean.length > 0 && isDestructiveOperationSafe('postWriteCanonicalize blank-after removal')) {
|
|
1497
|
-
blankLinesToClean.sort((a, b) => b - a).forEach((blankLineNum) => {
|
|
1498
|
-
try {
|
|
1499
|
-
const checkRep = ace.ace_getRep();
|
|
1500
|
-
if (!isLineSafeToModify(checkRep, blankLineNum, '[ep_data_tables:postWriteCanonicalize] blank-after')) {
|
|
1501
|
-
return;
|
|
1502
|
-
}
|
|
1503
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] removing blank line after repair', {
|
|
1504
|
-
blankLineNum, afterRow: capturedRow,
|
|
1505
|
-
});
|
|
1506
|
-
ace.ace_performDocumentReplaceRange([blankLineNum, 0], [blankLineNum + 1, 0], '');
|
|
1507
|
-
} catch (blankErr) {
|
|
1508
|
-
handleDomDesyncError(blankErr, 'postWriteCanonicalize blank-after');
|
|
1509
|
-
}
|
|
1510
|
-
});
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
} catch (finalCleanupErr) {
|
|
1514
|
-
console.debug('[ep_data_tables:postWriteCanonicalize] final blank cleanup error (non-fatal)', finalCleanupErr?.message);
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
try { ace.ace_fastIncorp(5); } catch (_) {}
|
|
1518
|
-
} catch (innerErr) {
|
|
1519
|
-
console.error('[ep_data_tables:postWriteCanonicalize] inner callback error', {
|
|
1520
|
-
error: innerErr?.message || innerErr,
|
|
1521
|
-
});
|
|
1522
|
-
}
|
|
1523
|
-
}, 'ep_data_tables:postwrite-canonicalize', true);
|
|
1524
|
-
} catch (aceCallErr) {
|
|
1525
|
-
// This catches errors from ace_callWithAce itself (Etherpad internal state corruption)
|
|
1526
|
-
console.warn('[ep_data_tables:postWriteCanonicalize] ace_callWithAce failed - editor state may need refresh', {
|
|
1527
|
-
error: aceCallErr?.message || aceCallErr,
|
|
1528
|
-
});
|
|
1529
|
-
}
|
|
1530
|
-
} finally {
|
|
1531
|
-
__epDT_postWriteScheduled.delete(nodeId);
|
|
1532
|
-
if (__epDT_compositionOriginalLine.tblId === capturedTblId) {
|
|
1533
|
-
__epDT_compositionOriginalLine = { tblId: null, lineNum: null, timestamp: 0 };
|
|
1534
|
-
}
|
|
1535
|
-
}
|
|
1536
|
-
}, 0);
|
|
1537
|
-
}
|
|
1538
|
-
// Stop here; let the next render reflect the canonicalized text.
|
|
1539
|
-
// BUT: if column operation is in progress, continue to render the table with current segments
|
|
1540
|
-
if (!skipMismatchReturn) {
|
|
1541
|
-
return cb();
|
|
1542
|
-
}
|
|
1543
|
-
// Otherwise, fall through to table building below
|
|
1544
|
-
} else {
|
|
744
|
+
// GRACEFUL DEGRADATION: Log the mismatch but render the table with actual segments
|
|
745
|
+
// This prevents the cascade of: repair → re-render → mismatch → repair → crash
|
|
746
|
+
// The table will display with its actual content (may differ from metadata column count)
|
|
747
|
+
// Content and styling are preserved - user can edit normally
|
|
748
|
+
// If user edits this row, handleDesktopCommitInput will normalize it at that time
|
|
749
|
+
console.warn('[ep_data_tables][diag] Segment/column mismatch (rendering as-is)', {
|
|
750
|
+
nodeId, lineNum,
|
|
751
|
+
actualSegs: htmlSegments.length,
|
|
752
|
+
expectedCols: rowMetadata.cols,
|
|
753
|
+
tblId: rowMetadata.tblId,
|
|
754
|
+
row: rowMetadata.row
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
// Always continue to render - don't return early, don't schedule repair
|
|
758
|
+
skipMismatchReturn = true;
|
|
1545
759
|
}
|
|
1546
760
|
|
|
1547
761
|
try {
|