From d6019fdb8aa8c99ffec46130fb8e05d8acd72cad Mon Sep 17 00:00:00 2001 From: Matt Voska Date: Thu, 1 Oct 2026 10:41:51 -0300 Subject: [PATCH] fix(ios): give each list item's marker array its own hash Each list item stores its markers as an NSArray attribute value. NSArray's -hash is its count, so every one-marker array hashes to 1, while the descriptors inside compare by identity, so no two of those arrays are equal. UIFoundation uniques attribute dictionaries in a hash table, so adding item N's marker probes past every earlier item's dictionary: rendering is quadratic in the number of list items, and memory grows with it. Store the markers in an NSArray subclass whose -hash is the hash of the item's own (last) marker. It is still an NSArray, so ListMarkerDrawer and TaskListTapUtils read it unchanged, and arrays that are equal still hash equal. --- .../ios/renderer/ListItemRenderer.m | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/react-native-enriched-markdown/ios/renderer/ListItemRenderer.m b/packages/react-native-enriched-markdown/ios/renderer/ListItemRenderer.m index 018014b63..cc554b2f6 100644 --- a/packages/react-native-enriched-markdown/ios/renderer/ListItemRenderer.m +++ b/packages/react-native-enriched-markdown/ios/renderer/ListItemRenderer.m @@ -19,6 +19,41 @@ @implementation ENRMListMarkerDescriptor @end +// NSArray's -hash is its count, so plain one-marker arrays all collide in the +// table that uniques attribute dictionaries, and each list item probes past +// every earlier one. Hashing by the item's own marker keeps rendering linear. +@interface ENRMListMarkerArray : NSArray +@end + +@implementation ENRMListMarkerArray { + NSArray *_markers; +} + +- (instancetype)initWithMarkers:(NSArray *)markers +{ + if (self = [super init]) { + _markers = [markers copy]; + } + return self; +} + +- (NSUInteger)count +{ + return _markers.count; +} + +- (id)objectAtIndex:(NSUInteger)index +{ + return _markers[index]; +} + +- (NSUInteger)hash +{ + return _markers.lastObject.hash; +} + +@end + @interface ListItemRenderer () - (void)applyCheckedDecorationsTo:(NSMutableAttributedString *)output range:(NSRange)range @@ -184,7 +219,9 @@ - (void)renderNodeContent:(MarkdownASTNode *)node NSArray *existingMarkers = [output attribute:ListItemMarkerStartAttribute atIndex:anchorLocation effectiveRange:NULL]; NSArray *markers = [existingMarkers isKindOfClass:[NSArray class]] ? [existingMarkers arrayByAddingObject:marker] : @[ marker ]; - [output addAttribute:ListItemMarkerStartAttribute value:markers range:anchorRange]; + [output addAttribute:ListItemMarkerStartAttribute + value:[[ENRMListMarkerArray alloc] initWithMarkers:markers] + range:anchorRange]; if (isTask && isChecked) { [self applyCheckedDecorationsTo:output range:itemRange nestingLevel:nestingLevel];