From 2a0cebb8516ba13cecb916beebb6302f325ba7b7 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Mon, 15 Jun 2026 19:42:53 +0200 Subject: [PATCH] improved: rtf roundtrip --- AppKit/CPTextView/_CPRTFParser.j | 19 ++++++++++++++++++- AppKit/CPTextView/_CPRTFProducer.j | 18 ++++++++++++++++-- Tests/Manual/CPTextView/AppController.j | 19 +++++-------------- 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/AppKit/CPTextView/_CPRTFParser.j b/AppKit/CPTextView/_CPRTFParser.j index 9cc99df34..a68d7af5f 100644 --- a/AppKit/CPTextView/_CPRTFParser.j +++ b/AppKit/CPTextView/_CPRTFParser.j @@ -178,7 +178,15 @@ var cp1252Map = { - (void)addTab:(float)location type:(CPTextTabType)type { - var tab = [[CPTextTab alloc] initWithType:type + var alignment = CPLeftTextAlignment; + if (type === CPCenterTabStopType || type === CPCenterTextAlignment) + alignment = CPCenterTextAlignment; + else if (type === CPRightTabStopType || type === CPRightTextAlignment) + alignment = CPRightTextAlignment; + else if (type === CPDecimalTabStopType) + alignment = CPRightTextAlignment; // Fallback alignment for decimal tab + + var tab = [[CPTextTab alloc] initWithType:alignment location:location]; if (!_tabChanged) @@ -512,6 +520,9 @@ var kRgsymRtf = { { if (_tableRows && [_tableRows count] > 0) { + // Flush active character styling runs before appending table layout changes + [self _flushCurrentRun]; + var headers = [_tableRows objectAtIndex:0]; var rows = [CPMutableArray array]; for (var idx = 1; idx < [_tableRows count]; idx++) @@ -536,6 +547,12 @@ var kRgsymRtf = { [_result appendAttributedString:tableAttrStr]; + // Update range offset of active attributes tracker + if (_currentRun) + { + _currentRun._range = CPMakeRange([_result length], 0); + } + _tableRows = nil; _currentRow = nil; _currentCellText = ""; diff --git a/AppKit/CPTextView/_CPRTFProducer.j b/AppKit/CPTextView/_CPRTFProducer.j index b10a11000..8d06ad104 100644 --- a/AppKit/CPTextView/_CPRTFProducer.j +++ b/AppKit/CPTextView/_CPRTFProducer.j @@ -357,14 +357,21 @@ function _points2twips(a) { return (a) * 20.0; } while ((tab = [enumerator nextObject])) { - switch ([tab tabStopType]) + var tabType = [tab respondsToSelector:@selector(tabStopType)] ? [tab tabStopType] : nil; + if (tabType === nil && [tab respondsToSelector:@selector(alignment)]) + tabType = [tab alignment]; + + switch (tabType) { case CPLeftTabStopType: + case CPLeftTextAlignment: break; case CPRightTabStopType: + case CPRightTextAlignment: headerString += @"\\tqr"; break; case CPCenterTabStopType: + case CPCenterTextAlignment: headerString += @"\\tqc"; break; case CPDecimalTabStopType: @@ -384,6 +391,13 @@ function _points2twips(a) { return (a) * 20.0; } { var unwrap = function(obj) { if (!obj) return null; + + // If the object contains the table structures, bypass unwrapping + if ((typeof obj.respondsToSelector === "function" && ([obj respondsToSelector:@selector(headers)] || [obj respondsToSelector:@selector(rows)])) || + obj.headers || obj._headers || obj.rows || obj._rows) { + return obj; + } + var unwrapped = null; if (typeof obj.respondsToSelector === "function") { if ([obj respondsToSelector:@selector(attachmentCell)]) { @@ -679,7 +693,7 @@ function _points2twips(a) { return (a) * 20.0; } substring = substring.replace(/\\/g, '\\\\'); substring = substring.replace(/\n/g, '\\par\n'); - substring = substring.replace(/\t/g, '\\tab'); + substring = substring.replace(/\t/g, '\\tab '); substring = substring.replace(/{/g, '\\{'); substring = substring.replace(/}/g, '\\}'); diff --git a/Tests/Manual/CPTextView/AppController.j b/Tests/Manual/CPTextView/AppController.j index de1d27100..a88dff417 100755 --- a/Tests/Manual/CPTextView/AppController.j +++ b/Tests/Manual/CPTextView/AppController.j @@ -406,20 +406,11 @@ var parser = [[_CPRTFParser alloc] init]; var roundTrippedString = [parser parseRTF:generatedRTF]; - // 5. Replace the editor's text storage with the round-tripped version - var editorStorage = [_textView textStorage]; - if (editorStorage && [editorStorage respondsToSelector:@selector(setAttributedString:)]) - { - [editorStorage setAttributedString:roundTrippedString]; - } - else - { - // Safe fallback sequence - [_textView setEditable:YES]; - [_textView setString:@""]; - [_textView insertText:roundTrippedString]; - [_textView setEditable:NO]; - } + // Safe fallback sequence + [_textView setEditable:YES]; + [_textView setString:@""]; + [_textView insertText:roundTrippedString]; + [_textView setEditable:NO]; // 6. Force a layout pass and render update [_textView setNeedsDisplay:YES];