From 5a3d6e30cb27cd30256555f6de5fb2f79135cd3a Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 17 Jun 2026 07:58:33 +0200 Subject: [PATCH] fixed: table sizing --- AppKit/CPTextView/_CPRTFParser.j | 68 +++++++++++-- AppKit/CPTextView/_CPTableTextAttachment.j | 105 ++++++++++----------- 2 files changed, 110 insertions(+), 63 deletions(-) diff --git a/AppKit/CPTextView/_CPRTFParser.j b/AppKit/CPTextView/_CPRTFParser.j index d8f95f027..6a0ba91ea 100644 --- a/AppKit/CPTextView/_CPRTFParser.j +++ b/AppKit/CPTextView/_CPRTFParser.j @@ -1076,6 +1076,25 @@ var kRgsymRtf = { if (numCols == 0 && [rows count] > 0) { numCols = [[rows objectAtIndex:0] count]; } + + // Convert raw header strings into CPAttributedStrings + var parsedHeaders = [CPMutableArray array]; + for (var c = 0; c < [headers count]; c++) { + var cellText = [headers objectAtIndex:c]; + [parsedHeaders addObject:[self parseTableCellMarkdown:cellText isHeader:YES]]; + } + + // Convert raw row strings into CPAttributedStrings + var parsedRows = [CPMutableArray array]; + for (var r = 0; r < [rows count]; r++) { + var rowData = [rows objectAtIndex:r]; + var parsedRow = [CPMutableArray array]; + for (var c = 0; c < [rowData count]; c++) { + var cellText = [rowData objectAtIndex:c]; + [parsedRow addObject:[self parseTableCellMarkdown:cellText isHeader:NO]]; + } + [parsedRows addObject:parsedRow]; + } var totalNaturalW = 0.0; var colNaturalWidths = []; @@ -1085,18 +1104,18 @@ var kRgsymRtf = { for (var c = 0; c < numCols; c++) { var cellW = 80.0; - if (c < headers.length) { - var parsedText = [self parseInlineMarkdown:headers[c] isHeader:YES headerLevel:3]; - [measureTextField setStringValue:[parsedText string]]; + if (c < [parsedHeaders count]) { + var parsedText = [parsedHeaders objectAtIndex:c]; + [measureTextField setStringValue:parsedText._string]; [measureTextField sizeToFit]; cellW = Math.max(cellW, CGRectGetWidth([measureTextField frame]) + 24.0); } - for (var r = 0; r < [rows count]; r++) { - var rowData = [rows objectAtIndex:r]; + for (var r = 0; r < [parsedRows count]; r++) { + var rowData = [parsedRows objectAtIndex:r]; if (c < [rowData count]) { - var parsedText = [self parseInlineMarkdown:rowData[c] isHeader:NO headerLevel:3]; - [measureTextField setStringValue:[parsedText string]]; + var parsedText = [rowData objectAtIndex:c]; + [measureTextField setStringValue:parsedText._string]; [measureTextField sizeToFit]; cellW = Math.max(cellW, CGRectGetWidth([measureTextField frame]) + 24.0); } @@ -1105,7 +1124,8 @@ var kRgsymRtf = { totalNaturalW += cellW; } - var matrixView = [[_CPTableTextAttachment alloc] initWithHeaders:headers rows:rows width:500.0]; + // Pass the parsed attributed strings to the attachment + var matrixView = [[_CPTableTextAttachment alloc] initWithHeaders:parsedHeaders rows:parsedRows width:500.0]; // Render utilizing the correct atomic attachment character string var tableAttrStr = [CPTextStorage attributedStringWithAttachment:matrixView]; @@ -1143,6 +1163,38 @@ var kRgsymRtf = { return result; } +// Parses a single table cell's Markdown, converting HTML breaks and handling list items ++ (CPAttributedString)parseTableCellMarkdown:(CPString)cellText isHeader:(BOOL)isHeader +{ + if (!cellText) { + return [[CPAttributedString alloc] initWithString:@""]; + } + + // Convert HTML line breaks to standard newlines + var cleanedText = cellText.replace(//gi, "\n"); + var lines = cleanedText.split(/\r?\n/); + var cellResult = [[CPMutableAttributedString alloc] initWithString:@""]; + + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim(); + + // Convert dash or asterisk lists inside the cell to bullets + var listMatch = line.match(/^(\*|-)\s+(.*)$/); + if (listMatch) { + line = " • " + listMatch[2]; + } + + var parsedLine = [self parseInlineMarkdown:line isHeader:isHeader headerLevel:3]; + [cellResult appendAttributedString:parsedLine]; + + if (i < lines.length - 1) { + [cellResult appendAttributedString:[[CPAttributedString alloc] initWithString:@"\n"]]; + } + } + + return cellResult; +} + + (BOOL)isTableHeaderLine:(CPString)line { var trimmed = line.trim(); diff --git a/AppKit/CPTextView/_CPTableTextAttachment.j b/AppKit/CPTextView/_CPTableTextAttachment.j index 7a7ffba92..d849c2e27 100644 --- a/AppKit/CPTextView/_CPTableTextAttachment.j +++ b/AppKit/CPTextView/_CPTableTextAttachment.j @@ -39,7 +39,7 @@ - (id)initWithHeaders:(CPArray)headers rows:(CPArray)rows width:(float)totalWidth { - return [self initWithHeaders:headers rows:rows width:totalWidth isEditable:YES acceptsRichText:NO]; + return [self initWithHeaders:headers rows:rows width:totalWidth isEditable:YES acceptsRichText:YES]; } - (id)initWithHeaders:(CPArray)headers rows:(CPArray)rows width:(float)totalWidth isEditable:(BOOL)isEditable acceptsRichText:(BOOL)acceptsRichText @@ -246,6 +246,10 @@ var textContainer = [[CPTextContainer alloc] initWithContainerSize:CGSizeMake(initialWidth - 8, 1e7)]; var textView = [[CPTextView alloc] initWithFrame:CGRectMake(4, 2, initialWidth - 8, initialHeight - 4) textContainer:textContainer]; + + // Set explicit zero margins inside the text view to keep height measurements aligned with usedRect + [textView setTextContainerInset:CGSizeMake(0, 0)]; + [textView setEditable:_isEditable]; [textView setSelectable:YES]; [textView setBackgroundColor:[CPColor clearColor]]; @@ -264,21 +268,11 @@ [textView setFont:cellFont]; [textView setTextColor:[CPColor blackColor]]; - // Populate either rich text or plain text safely - if (text && [text isKindOfClass:[CPAttributedString class]]) - { - [[textView textStorage] setAttributedString:text]; - } - else if (text) - { - [textView setString:String(text)]; - } - else - { - [textView setString:@""]; - } - + if (text) + [textView insertText:text]; + [cellContainer addSubview:textView]; + return cellContainer; } @@ -441,32 +435,55 @@ for (var c = 0; c < numCols; c++) { var idx = startIndex + c; + if (idx < [subviews count]) { var cellView = [subviews objectAtIndex:idx]; var textView = [self getTextViewFromCell:cellView]; - if (textView) { + + if (textView) + { var targetWidth = Math.max(10.0, colWidths[c] - 8); - [[textView textContainer] setContainerSize:CGSizeMake(targetWidth, 1e7)]; + + // Update frame size directly so that textContainer auto-resizes. + // Set a large temporary height to allow accurate wrapping measurements. + [textView setFrameSize:CGSizeMake(targetWidth, 1e7)]; var layoutManager = [textView layoutManager]; - var usedRect = layoutManager ? [layoutManager usedRectForTextContainer:[textView textContainer]] : nil; - var wrappedHeight = (usedRect ? CGRectGetHeight(usedRect) : 0.0) + 12.0; - if (wrappedHeight > maxCellHeight) { - maxCellHeight = wrappedHeight; + if (layoutManager) + { + // FORCE LAYOUT RECALCULATION: + // Because the width changed, we must force the lazy layout manager + // to synchronously calculate glyphs and wraps at the new width. + [layoutManager glyphRangeForTextContainer:[textView textContainer]]; } + + var usedRect = layoutManager ? [layoutManager usedRectForTextContainer:[textView textContainer]] : nil; + var textHeight = usedRect ? CGRectGetHeight(usedRect) : 0.0; + + // Exact height + 8.0px padding (4.0px top, 4.0px bottom margin) inside the cell + var wrappedHeight = textHeight + 8.0; + + if (wrappedHeight > maxCellHeight) + maxCellHeight = wrappedHeight; } } } var currentX = 0; - for (var c = 0; c < numCols; c++) { + + for (var c = 0; c < numCols; c++) + { var idx = startIndex + c; - if (idx < [subviews count]) { + + if (idx < [subviews count]) + { var cellView = [subviews objectAtIndex:idx]; [cellView setFrame:CGRectMake(currentX, currentY, colWidths[c], maxCellHeight)]; var textView = [self getTextViewFromCell:cellView]; - if (textView) { + + if (textView) + { var targetWidth = Math.max(10.0, colWidths[c] - 8); var textY = 4.0; var finalTextViewHeight = maxCellHeight - 8.0; @@ -474,9 +491,9 @@ } var cellSubviews = [cellView subviews]; - if ([cellSubviews count] > 0) { + + if ([cellSubviews count] > 0) [[cellSubviews objectAtIndex:0] setFrame:CGRectMake(0, 0, colWidths[c], maxCellHeight)]; - } } currentX += colWidths[c]; } @@ -484,14 +501,17 @@ return maxCellHeight; }; - if (currentHeaders && [currentHeaders count] > 0) { + if (currentHeaders && [currentHeaders count] > 0) + { var headerHeight = layoutRow(cellIndex); cellIndex += numCols; currentY += headerHeight; } - if (currentRows) { - for (var r = 0; r < [currentRows count]; r++) { + if (currentRows) + { + for (var r = 0; r < [currentRows count]; r++) + { var rowHeight = layoutRow(cellIndex); cellIndex += numCols; currentY += rowHeight; @@ -500,35 +520,10 @@ // GUARD FRAME SIZE MUTATIONS var currentSize = [self frame].size; + if (ABS(currentSize.width - newWidth) > 0.1 || ABS(currentSize.height - currentY) > 0.1) { [self setFrameSize:CGSizeMake(newWidth, currentY)]; - - // we need to re-layout the textview here. - // but this is not easy as the layout engine is not re-entrant - // this does not work: (delay does not matter), layout is always off -// setTimeout(function() { -// var textView = [self superview]; -// -// if (textView && [textView isKindOfClass:[CPTextView class]]) -// { -// var layoutManager = [textView layoutManager]; -// -// if (layoutManager) -// { -// var charRange = [self _findCharacterRangeInLayoutManager:layoutManager]; -// -// if (charRange && charRange.location !== CPNotFound) -// { -// [layoutManager invalidateLayoutForCharacterRange:charRange isSoft:NO actualCharacterRange:nil]; -// [layoutManager invalidateDisplayForGlyphRange:charRange]; -// [layoutManager _validateLayoutAndGlyphs]; -// [textView sizeToFit]; -// } -// } -// } -// }, 0); - } _isResizing = NO;