Skip to content

Commit a5cefe7

Browse files
committed
feat(ios): add streamingConfig with progressive table mode for GFM streaming
1 parent 277ad44 commit a5cefe7

11 files changed

Lines changed: 176 additions & 15 deletions

apps/example/src/StreamingMarkdownSimulator.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,9 @@ export default function StreamingMarkdownSimulator() {
148148
markdownStyle={markdownStyle}
149149
md4cFlags={{ latexMath: true }}
150150
streamingAnimation
151+
streamingConfig={{
152+
tableMode: 'progressive',
153+
}}
151154
/>
152155
</View>
153156

ios/EnrichedMarkdown.mm

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
#import "EditMenuUtils.h"
1010

1111
#import "ENRMFeatureFlags.h"
12-
#import "ENRMUIKit.h"
1312

1413
#if ENRICHED_MARKDOWN_MATH
1514
#import "ENRMMathContainerView.h"
@@ -89,6 +88,7 @@ @implementation EnrichedMarkdown {
8988
BOOL _selectable;
9089
BOOL _enableLinkPreview;
9190
BOOL _streamingAnimation;
91+
ENRMTableStreamingMode _tableStreamingMode;
9292

9393
NSArray<NSString *> *_contextMenuItemTexts;
9494
NSArray<NSString *> *_contextMenuItemIcons;
@@ -123,6 +123,7 @@ - (instancetype)initWithFrame:(CGRect)frame
123123
_selectable = YES;
124124
_enableLinkPreview = YES;
125125
_streamingAnimation = NO;
126+
_tableStreamingMode = ENRMTableStreamingModeHidden;
126127

127128
_fontScaleObserver = [[FontScaleObserver alloc] init];
128129
__weak EnrichedMarkdown *weakSelf = self;
@@ -185,7 +186,10 @@ - (void)configureSegmentViewRegistry
185186
return view;
186187
}
187188
updateView:^(RCTUIView *view, ENRMRenderedSegment *segment) {
188-
[(TableContainerView *)view applyTableNode:segment.tableSegment.tableNode];
189+
EnrichedMarkdown *strongSelf = weakSelf;
190+
if (strongSelf) {
191+
[strongSelf updateTableView:(TableContainerView *)view withSegment:segment.tableSegment];
192+
}
189193
}]];
190194

191195
#if ENRICHED_MARKDOWN_MATH
@@ -347,10 +351,11 @@ - (void)renderMarkdownContent:(NSString *)markdownString
347351
CGFloat maxFontSizeMultiplier = _maxFontSizeMultiplier;
348352
BOOL allowTrailingMargin = _allowTrailingMargin;
349353
BOOL streamingAnimation = _streamingAnimation;
354+
ENRMTableStreamingMode tableStreamingMode = _tableStreamingMode;
350355

351356
dispatch_async(_renderQueue, ^{
352357
NSString *renderableMarkdown =
353-
streamingAnimation ? ENRMRenderableMarkdownForStreaming(markdownString) : markdownString;
358+
streamingAnimation ? ENRMRenderableMarkdownForStreaming(markdownString, tableStreamingMode) : markdownString;
354359
if (renderableMarkdown.length == 0) {
355360
dispatch_async(dispatch_get_main_queue(), ^{
356361
if (renderId == self->_currentRenderId) {
@@ -405,7 +410,7 @@ - (void)renderMarkdownSynchronously:(NSString *)markdownString
405410
_blockAsyncRender = YES;
406411
_cachedMarkdown = [markdownString copy];
407412
NSString *renderableMarkdown =
408-
_streamingAnimation ? ENRMRenderableMarkdownForStreaming(markdownString) : markdownString;
413+
_streamingAnimation ? ENRMRenderableMarkdownForStreaming(markdownString, _tableStreamingMode) : markdownString;
409414
_renderedMarkdown = [renderableMarkdown copy];
410415

411416
if (renderableMarkdown.length == 0) {
@@ -458,11 +463,6 @@ - (void)applyRenderedSegments:(NSArray *)renderedSegments renderedMarkdown:(NSSt
458463
[self computeSegmentLayoutForWidth:self.bounds.size.width applyFrames:YES];
459464
[self layoutIfNeeded];
460465
[self requestHeightUpdate];
461-
} else if (_streamingAnimation) {
462-
CGSize measured = [self measureSize:self.bounds.size.width];
463-
if (needsHeightUpdate(measured, self.bounds)) {
464-
[self requestHeightUpdate];
465-
}
466466
} else {
467467
CGSize measured = [self measureSize:self.bounds.size.width];
468468
if (needsHeightUpdate(measured, self.bounds)) {
@@ -561,6 +561,39 @@ - (TableContainerView *)createTableViewForSegment:(ENRMTableSegment *)tableSegme
561561
return tableView;
562562
}
563563

564+
- (void)updateTableView:(TableContainerView *)view withSegment:(ENRMTableSegment *)tableSegment
565+
{
566+
NSUInteger previousRowCount = view.rowCount;
567+
[view applyTableNode:tableSegment.tableNode];
568+
569+
#if !TARGET_OS_OSX
570+
if (!_streamingAnimation || view.rowCount <= previousRowCount) {
571+
return;
572+
}
573+
574+
// The grid container is inside the scroll view: TableContainerView > UIScrollView > gridContainer.
575+
// After applyTableNode:, cells are laid out sequentially — each row has colCount cell-background subviews.
576+
RCTUIView *scrollView = view.subviews.firstObject;
577+
RCTUIView *gridContainer = scrollView.subviews.firstObject;
578+
if (!gridContainer || gridContainer.subviews.count == 0 || view.rowCount == 0) {
579+
return;
580+
}
581+
582+
NSUInteger colCount = gridContainer.subviews.count / view.rowCount;
583+
if (colCount == 0) {
584+
return;
585+
}
586+
587+
NSUInteger firstNewCellIndex = previousRowCount * colCount;
588+
NSArray<RCTUIView *> *subviews = gridContainer.subviews;
589+
for (NSUInteger i = firstNewCellIndex; i < subviews.count; i++) {
590+
RCTUIView *cellView = subviews[i];
591+
cellView.alpha = 0.0;
592+
[UIView animateWithDuration:0.20 animations:^{ cellView.alpha = 1.0; }];
593+
}
594+
#endif
595+
}
596+
564597
#if ENRICHED_MARKDOWN_MATH
565598
- (ENRMMathContainerView *)createMathViewForSegment:(ENRMMathSegment *)mathSegment
566599
{
@@ -697,6 +730,15 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
697730
}
698731
}
699732

733+
BOOL streamingConfigChanged = NO;
734+
if (newViewProps.streamingConfig.tableMode != oldViewProps.streamingConfig.tableMode) {
735+
NSString *tableModeStr = [[NSString alloc] initWithUTF8String:newViewProps.streamingConfig.tableMode.c_str()];
736+
_tableStreamingMode = [tableModeStr isEqualToString:@"progressive"] ? ENRMTableStreamingModeProgressive
737+
: ENRMTableStreamingModeHidden;
738+
streamingConfigChanged = YES;
739+
_dirtyFlags |= ENRMDirtyForceHeight;
740+
}
741+
700742
if (ENRMContextMenuItemsChanged(oldViewProps.contextMenuItems, newViewProps.contextMenuItems)) {
701743
_contextMenuItemTexts = ENRMContextMenuTextsFromItems(newViewProps.contextMenuItems);
702744
_contextMenuItemIcons = ENRMContextMenuIconsFromItems(newViewProps.contextMenuItems);
@@ -722,7 +764,7 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
722764
}
723765

724766
if (markdownChanged || stylePropChanged || md4cFlagsChanged || allowTrailingMarginChanged ||
725-
streamingAnimationChanged) {
767+
streamingAnimationChanged || streamingConfigChanged) {
726768
NSString *markdownString = [[NSString alloc] initWithUTF8String:newViewProps.markdown.c_str()];
727769
[self renderMarkdownContent:markdownString];
728770
}

ios/utils/StreamingMarkdownFilter.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@
44

55
NS_ASSUME_NONNULL_BEGIN
66

7+
typedef NS_ENUM(NSInteger, ENRMTableStreamingMode) {
8+
ENRMTableStreamingModeHidden = 0,
9+
ENRMTableStreamingModeProgressive,
10+
};
11+
712
#ifdef __cplusplus
813
extern "C" {
914
#endif
1015

11-
NSString *ENRMRenderableMarkdownForStreaming(NSString *markdown);
16+
NSString *ENRMRenderableMarkdownForStreaming(NSString *markdown, ENRMTableStreamingMode tableMode);
1217

1318
#ifdef __cplusplus
1419
}

ios/utils/StreamingMarkdownFilter.m

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,45 @@ static BOOL ENRMLineLooksLikeTableRow(NSString *line)
1616
return [trimmed hasPrefix:@"|"] && [trimmed containsString:@"|"];
1717
}
1818

19+
static NSUInteger ENRMPipeCount(NSString *line)
20+
{
21+
NSUInteger count = 0;
22+
for (NSUInteger i = 0; i < line.length; i++) {
23+
if ([line characterAtIndex:i] == '|') {
24+
count++;
25+
}
26+
}
27+
return count;
28+
}
29+
30+
static BOOL ENRMLineLooksLikeTableSeparator(NSString *line)
31+
{
32+
NSString *trimmed = [line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
33+
if (trimmed.length == 0) {
34+
return NO;
35+
}
36+
if ([trimmed characterAtIndex:0] != '|') {
37+
return NO;
38+
}
39+
BOOL hasTripleDash = NO;
40+
NSUInteger dashRun = 0;
41+
for (NSUInteger i = 0; i < trimmed.length; i++) {
42+
unichar ch = [trimmed characterAtIndex:i];
43+
if (ch == '-') {
44+
dashRun++;
45+
if (dashRun >= 3) {
46+
hasTripleDash = YES;
47+
}
48+
} else {
49+
dashRun = 0;
50+
if (ch != '|' && ch != ':' && ch != ' ') {
51+
return NO;
52+
}
53+
}
54+
}
55+
return hasTripleDash;
56+
}
57+
1958
static NSUInteger ENRMLineStartOffset(NSArray<NSString *> *lines, NSUInteger lineIndex)
2059
{
2160
NSUInteger offset = 0;
@@ -45,7 +84,7 @@ static NSUInteger ENRMLineStartOffset(NSArray<NSString *> *lines, NSUInteger lin
4584
return [markdown substringToIndex:offset];
4685
}
4786

48-
static NSString *ENRMRemovePendingStreamingTableBlock(NSString *markdown)
87+
static NSString *ENRMRemovePendingStreamingTableBlock(NSString *markdown, ENRMTableStreamingMode tableMode)
4988
{
5089
NSArray<NSString *> *lines = [markdown componentsSeparatedByString:@"\n"];
5190
NSInteger lastNonBlankLineIndex = -1;
@@ -85,12 +124,36 @@ static NSUInteger ENRMLineStartOffset(NSArray<NSString *> *lines, NSUInteger lin
85124
return markdown;
86125
}
87126

127+
if (tableMode == ENRMTableStreamingModeProgressive) {
128+
NSInteger tableLineCount = lastNonBlankLineIndex - blockStartIndex + 1;
129+
130+
// Need at least header + separator to show anything.
131+
if (tableLineCount < 2 || !ENRMLineLooksLikeTableSeparator(lines[(NSUInteger)blockStartIndex + 1])) {
132+
NSUInteger offset = ENRMLineStartOffset(lines, (NSUInteger)blockStartIndex);
133+
return [markdown substringToIndex:offset];
134+
}
135+
136+
// Trim the last data row if it's incomplete: either doesn't end with '|'
137+
// or has fewer pipe characters than the header (mid-cell streaming).
138+
if (tableLineCount > 2) {
139+
NSString *lastRow = lines[(NSUInteger)lastNonBlankLineIndex];
140+
NSString *lastRowTrimmed = [lastRow stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
141+
NSString *headerRow = lines[(NSUInteger)blockStartIndex];
142+
if (![lastRowTrimmed hasSuffix:@"|"] || ENRMPipeCount(lastRow) < ENRMPipeCount(headerRow)) {
143+
NSUInteger offset = ENRMLineStartOffset(lines, (NSUInteger)lastNonBlankLineIndex);
144+
return [markdown substringToIndex:offset];
145+
}
146+
}
147+
148+
return markdown;
149+
}
150+
88151
NSUInteger offset = ENRMLineStartOffset(lines, (NSUInteger)blockStartIndex);
89152
return [markdown substringToIndex:offset];
90153
}
91154

92-
NSString *ENRMRenderableMarkdownForStreaming(NSString *markdown)
155+
NSString *ENRMRenderableMarkdownForStreaming(NSString *markdown, ENRMTableStreamingMode tableMode)
93156
{
94157
NSString *withoutPendingMath = ENRMRemovePendingStreamingMathBlock(markdown);
95-
return ENRMRemovePendingStreamingTableBlock(withoutPendingMath);
158+
return ENRMRemovePendingStreamingTableBlock(withoutPendingMath, tableMode);
96159
}

ios/views/TableContainerView.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ typedef void (^TableLinkPressBlock)(NSString *url);
2626

2727
@property (nonatomic, assign) BOOL enableLinkPreview;
2828

29+
@property (nonatomic, readonly) NSUInteger rowCount;
30+
2931
@end
3032

3133
NS_ASSUME_NONNULL_END

ios/views/TableContainerView.m

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,11 @@ - (NSString *)extractPlainTextFromNode:(MarkdownASTNode *)node
167167
return [buffer copy];
168168
}
169169

170+
- (NSUInteger)rowCount
171+
{
172+
return _rows.count;
173+
}
174+
170175
- (void)applyTableNode:(MarkdownASTNode *)tableNode
171176
{
172177
[[_gridContainer subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)];

src/EnrichedMarkdownNativeComponent.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,10 @@ export interface Md4cFlagsInternal {
226226
latexMath: boolean;
227227
}
228228

229+
interface StreamingConfigInternal {
230+
tableMode: string;
231+
}
232+
229233
export interface NativeProps extends ViewProps {
230234
/**
231235
* Markdown content to render.
@@ -322,6 +326,10 @@ export interface NativeProps extends ViewProps {
322326
* @default false
323327
*/
324328
streamingAnimation?: CodegenTypes.WithDefault<boolean, false>;
329+
/**
330+
* Fine-grained control over streaming behavior for block-level elements.
331+
*/
332+
streamingConfig?: StreamingConfigInternal;
325333
/**
326334
* Controls how spoiler text is displayed before being revealed.
327335
* - 'particles' (default): animated particle overlay.

src/EnrichedMarkdownTextNativeComponent.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,10 @@ export interface Md4cFlagsInternal {
226226
latexMath: boolean;
227227
}
228228

229+
interface StreamingConfigInternal {
230+
tableMode: string;
231+
}
232+
229233
export interface NativeProps extends ViewProps {
230234
/**
231235
* Markdown content to render.
@@ -322,6 +326,10 @@ export interface NativeProps extends ViewProps {
322326
* @default false
323327
*/
324328
streamingAnimation?: CodegenTypes.WithDefault<boolean, false>;
329+
/**
330+
* Fine-grained control over streaming behavior for block-level elements.
331+
*/
332+
streamingConfig?: StreamingConfigInternal;
325333
/**
326334
* Controls how spoiler text is displayed before being revealed.
327335
* - 'particles' (default): animated particle overlay.

src/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export { default as EnrichedMarkdownText } from './native/EnrichedMarkdownText';
22
export type {
33
EnrichedMarkdownTextProps,
4+
StreamingConfig,
45
MarkdownStyle,
56
Md4cFlags,
67
ContextMenuItem as TextContextMenuItem,

src/native/EnrichedMarkdownText.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { NativeSyntheticEvent } from 'react-native';
77
import type { MarkdownStyle, Md4cFlags } from '../types/MarkdownStyle';
88
import type {
99
EnrichedMarkdownTextProps,
10+
StreamingConfig,
1011
ContextMenuItem,
1112
} from '../types/MarkdownTextProps';
1213
import type {
@@ -17,7 +18,7 @@ import type {
1718
} from '../types/events';
1819

1920
export type { MarkdownStyle, Md4cFlags };
20-
export type { EnrichedMarkdownTextProps, ContextMenuItem };
21+
export type { EnrichedMarkdownTextProps, StreamingConfig, ContextMenuItem };
2122
export type { LinkPressEvent, LinkLongPressEvent, TaskListItemPressEvent };
2223

2324
const defaultMd4cFlags: Md4cFlags = {
@@ -40,6 +41,7 @@ export const EnrichedMarkdownText = ({
4041
allowTrailingMargin = false,
4142
flavor = 'commonmark',
4243
streamingAnimation = false,
44+
streamingConfig,
4345
spoilerOverlay = 'particles',
4446
contextMenuItems,
4547
selectionColor,
@@ -122,6 +124,9 @@ export const EnrichedMarkdownText = ({
122124
[onTaskListItemPress]
123125
);
124126

127+
const tableMode = streamingConfig?.tableMode ?? 'hidden';
128+
const normalizedStreamingConfig = useMemo(() => ({ tableMode }), [tableMode]);
129+
125130
const sharedProps = {
126131
markdown,
127132
markdownStyle: normalizedStyle,
@@ -135,6 +140,7 @@ export const EnrichedMarkdownText = ({
135140
maxFontSizeMultiplier,
136141
allowTrailingMargin,
137142
streamingAnimation,
143+
streamingConfig: normalizedStreamingConfig,
138144
spoilerOverlay,
139145
style: containerStyle,
140146
contextMenuItems: nativeContextMenuItems,

0 commit comments

Comments
 (0)