From 1d789375f07ba322ec8ced4da044d458ed8725b5 Mon Sep 17 00:00:00 2001 From: Ernest Date: Mon, 17 Aug 2026 13:53:16 +0200 Subject: [PATCH 01/11] feat: ios --- .../ios/EnrichedMarkdown.mm | 52 +++- .../ios/internals/ENRMViewFreeMeasurement.h | 13 +- .../ios/renderer/AttributedRenderer.h | 7 + .../ios/renderer/AttributedRenderer.m | 19 +- .../ios/utils/ENRMSegmentHeightMeasurer.h | 32 +++ .../ios/utils/ENRMSegmentHeightMeasurer.mm | 76 +++++ .../ios/utils/ENRMTextRenderer.h | 8 + .../ios/utils/ENRMTextRenderer.m | 34 ++- .../ios/utils/RenderedMarkdownSegment.h | 10 +- .../ios/utils/RenderedMarkdownSegment.m | 20 ++ .../ios/utils/SegmentRenderer.h | 10 +- .../ios/utils/SegmentRenderer.m | 29 +- .../ios/views/ENRMBlockquoteContainerView.h | 66 +++++ .../ios/views/ENRMBlockquoteContainerView.mm | 262 ++++++++++++++++++ .../ios/views/ENRMContainerNodeView.h | 52 ++++ .../ios/views/ENRMContainerNodeView.mm | 183 ++++++++++++ 16 files changed, 853 insertions(+), 20 deletions(-) create mode 100644 packages/react-native-enriched-markdown/ios/utils/ENRMSegmentHeightMeasurer.h create mode 100644 packages/react-native-enriched-markdown/ios/utils/ENRMSegmentHeightMeasurer.mm create mode 100644 packages/react-native-enriched-markdown/ios/views/ENRMBlockquoteContainerView.h create mode 100644 packages/react-native-enriched-markdown/ios/views/ENRMBlockquoteContainerView.mm create mode 100644 packages/react-native-enriched-markdown/ios/views/ENRMContainerNodeView.h create mode 100644 packages/react-native-enriched-markdown/ios/views/ENRMContainerNodeView.mm diff --git a/packages/react-native-enriched-markdown/ios/EnrichedMarkdown.mm b/packages/react-native-enriched-markdown/ios/EnrichedMarkdown.mm index ed8b53d0a..2937dca03 100644 --- a/packages/react-native-enriched-markdown/ios/EnrichedMarkdown.mm +++ b/packages/react-native-enriched-markdown/ios/EnrichedMarkdown.mm @@ -18,6 +18,7 @@ #if ENRICHED_MARKDOWN_MATH #import "ENRMMathContainerView.h" #endif +#import "ENRMBlockquoteContainerView.h" #import "ENRMCodeBlockContainerView.h" #import "ENRMSpoilerCapable.h" #import "ENRMSpoilerOverlayView.h" @@ -288,9 +289,49 @@ - (void)configureSegmentViewRegistry [codeBlockView applyCodeBlockNode:segment.codeBlockSegment.codeBlockNode]; }]]; + [handlers addObject:[ENRMSegmentViewHandler handlerWithKind:ENRMSegmentKindBlockquote + matchesView:^BOOL(RCTUIView *view, ENRMRenderedSegment *segment) { + return [view isKindOfClass:[ENRMBlockquoteContainerView class]]; + } + createView:^RCTUIView *(ENRMRenderedSegment *segment) { + EnrichedMarkdown *strongSelf = weakSelf; + if (!strongSelf) { + return [[RCTUIView alloc] init]; + } + + ENRMBlockquoteContainerView *view = + [strongSelf createBlockquoteViewForSegment:segment.blockquoteSegment]; + [strongSelf animateBlockViewIfNeeded:view]; + return view; + } + updateView:^(RCTUIView *view, ENRMRenderedSegment *segment) { + [(ENRMBlockquoteContainerView *)view + applyBlockquoteNode:segment.blockquoteSegment.blockquoteNode]; + }]]; + _segmentViewRegistry = [[ENRMSegmentViewRegistry alloc] initWithHandlers:handlers]; } +- (ENRMBlockquoteContainerView *)createBlockquoteViewForSegment:(ENRMBlockquoteSegment *)blockquoteSegment +{ + ENRMBlockquoteContainerView *view = [[ENRMBlockquoteContainerView alloc] initWithConfig:_config]; + + __weak EnrichedMarkdown *weakSelf = self; + view.onLinkPress = ^(NSString *url) { + EnrichedMarkdown *strongSelf = weakSelf; + if (strongSelf && url) + [strongSelf emitLinkPress:url]; + }; + view.onLinkLongPress = ^(NSString *url) { + EnrichedMarkdown *strongSelf = weakSelf; + if (strongSelf && url) + [strongSelf emitLinkLongPress:url]; + }; + + [view applyBlockquoteNode:blockquoteSegment.blockquoteNode]; + return view; +} + - (CGSize)computeSegmentLayoutForWidth:(CGFloat)width applyFrames:(BOOL)applyFrames { if (_segmentViews.count == 0) @@ -353,6 +394,10 @@ - (CGSize)computeSegmentLayoutForWidth:(CGFloat)width applyFrames:(BOOL)applyFra yOffset += _config.codeBlockMarginTop; segmentHeight = [(ENRMCodeBlockContainerView *)segment measureHeight:width]; maxContentWidth = width; + } else if ([segment isKindOfClass:[ENRMBlockquoteContainerView class]]) { + yOffset += _config.blockquoteMarginTop; + segmentHeight = [(ENRMBlockquoteContainerView *)segment measureHeight:width]; + maxContentWidth = width; } if (applyFrames) { @@ -390,6 +435,8 @@ - (CGSize)computeSegmentLayoutForWidth:(CGFloat)width applyFrames:(BOOL)applyFra #endif else if ([segment isKindOfClass:[ENRMCodeBlockContainerView class]] && shouldAddBottomMargin) { yOffset += _config.codeBlockMarginBottom; + } else if ([segment isKindOfClass:[ENRMBlockquoteContainerView class]] && shouldAddBottomMargin) { + yOffset += _config.blockquoteMarginBottom; } }]; @@ -595,7 +642,8 @@ - (void)renderMarkdownContent:(NSString *)markdownString return NO; renderedSegments = ENRMRenderSegmentsFromAST(ast, config, allowTrailingMargin, allowFontScaling, - maxFontSizeMultiplier, lineBreakStrategy); + maxFontSizeMultiplier, lineBreakStrategy, + /*blockquoteContent*/ NO); for (ENRMRenderedSegment *segment in renderedSegments) { if (segment.kind == ENRMSegmentKindText && segment.textResult) { ENRMApplyWritingDirectionMode(segment.textResult.attributedText, writingDirectionMode, @@ -621,7 +669,7 @@ - (NSArray *)parseAndRenderSegments:(NSString *)markdownString NSArray *segments = ENRMRenderSegmentsFromAST(ast, _config, _allowTrailingMargin, _fontScaleObserver.allowFontScaling, - _maxFontSizeMultiplier, _lineBreakStrategy); + _maxFontSizeMultiplier, _lineBreakStrategy, /*blockquoteContent*/ NO); for (ENRMRenderedSegment *segment in segments) { if (segment.kind == ENRMSegmentKindText && segment.textResult) { ENRMApplyWritingDirectionMode(segment.textResult.attributedText, _writingDirectionMode, _resolvedLayoutDirection); diff --git a/packages/react-native-enriched-markdown/ios/internals/ENRMViewFreeMeasurement.h b/packages/react-native-enriched-markdown/ios/internals/ENRMViewFreeMeasurement.h index 1905d4505..0a58b4e43 100644 --- a/packages/react-native-enriched-markdown/ios/internals/ENRMViewFreeMeasurement.h +++ b/packages/react-native-enriched-markdown/ios/internals/ENRMViewFreeMeasurement.h @@ -1,5 +1,6 @@ #pragma once +#import "ENRMBlockquoteContainerView.h" #import "ENRMCodeBlockContainerView.h" #import "ENRMFeatureFlags.h" #import "ENRMMarkdownParser.h" @@ -259,7 +260,7 @@ static inline CGSize ENRMMeasureSegmentedMarkdownViewFree(const PropsT &typedPro NSArray *segments = ENRMRenderSegmentsFromAST(ast, config, typedProps.allowTrailingMargin, typedProps.allowFontScaling, - typedProps.maxFontSizeMultiplier, lineBreakStrategy); + typedProps.maxFontSizeMultiplier, lineBreakStrategy, /*blockquoteContent*/ NO); for (ENRMRenderedSegment *segment in segments) { if (segment.kind == ENRMSegmentKindText && segment.textResult) { ENRMApplyWritingDirectionMode(segment.textResult.attributedText, writingDirectionMode, resolvedLayoutDirection); @@ -305,6 +306,16 @@ static inline CGSize ENRMMeasureSegmentedMarkdownViewFree(const PropsT &typedPro if (shouldAddBottomMargin) { yOffset += config.codeBlockMarginBottom; } + } else if (segment.kind == ENRMSegmentKindBlockquote && segment.blockquoteSegment) { + yOffset += config.blockquoteMarginTop; + yOffset += [ENRMBlockquoteContainerView measureHeightForBlockquoteNode:segment.blockquoteSegment.blockquoteNode + config:config + maxWidth:maxWidth + pointScaleFactor:pointScaleFactor]; + maxContentWidth = maxWidth; + if (shouldAddBottomMargin) { + yOffset += config.blockquoteMarginBottom; + } } #if ENRICHED_MARKDOWN_MATH else if (segment.kind == ENRMSegmentKindMath && segment.mathSegment) { diff --git a/packages/react-native-enriched-markdown/ios/renderer/AttributedRenderer.h b/packages/react-native-enriched-markdown/ios/renderer/AttributedRenderer.h index 7601efb43..b0397e50a 100644 --- a/packages/react-native-enriched-markdown/ios/renderer/AttributedRenderer.h +++ b/packages/react-native-enriched-markdown/ios/renderer/AttributedRenderer.h @@ -6,6 +6,13 @@ @interface AttributedRenderer : NSObject - (instancetype)initWithConfig:(id)config; - (NSMutableAttributedString *)renderRoot:(MarkdownASTNode *)root context:(RenderContext *)context; +// Renders a blockquote's own content (the nodes left after nested quotes/code blocks are split +// out) with a blockquote baseline block style instead of paragraph: text picks up the quote's +// font/color and paragraphs render tight (no paragraph margins), matching the commonmark path. +// It draws no box; the ENRMBlockquoteContainerView draws the border/background/padding. +- (NSMutableAttributedString *)renderRoot:(MarkdownASTNode *)root + context:(RenderContext *)context + asBlockquoteContent:(BOOL)asBlockquoteContent; - (CGFloat)getLastElementMarginBottom; - (void)setAllowTrailingMargin:(BOOL)allow; @end diff --git a/packages/react-native-enriched-markdown/ios/renderer/AttributedRenderer.m b/packages/react-native-enriched-markdown/ios/renderer/AttributedRenderer.m index 83c8b41de..34706371a 100644 --- a/packages/react-native-enriched-markdown/ios/renderer/AttributedRenderer.m +++ b/packages/react-native-enriched-markdown/ios/renderer/AttributedRenderer.m @@ -33,13 +33,30 @@ - (instancetype)initWithConfig:(StyleConfig *)config * Sets the baseline global style and initiates the recursive traversal. */ - (NSMutableAttributedString *)renderRoot:(MarkdownASTNode *)root context:(RenderContext *)context +{ + return [self renderRoot:root context:context asBlockquoteContent:NO]; +} + +- (NSMutableAttributedString *)renderRoot:(MarkdownASTNode *)root + context:(RenderContext *)context + asBlockquoteContent:(BOOL)asBlockquoteContent { if (!root) return [[NSMutableAttributedString alloc] init]; // 1. Establish the global baseline style. // This ensures that leaf nodes (like Text) have valid attributes if they appear at the root. - [context setBlockStyle:BlockTypeParagraph font:_config.paragraphFont color:_config.paragraphColor headingLevel:0]; + // A blockquote's own content uses the blockquote baseline so its paragraphs render tight and + // pick up the quote's font/color (currentBlockType == Blockquote), mirroring commonmark. + if (asBlockquoteContent) { + context.blockquoteDepth = context.blockquoteDepth + 1; + [context setBlockStyle:BlockTypeBlockquote + font:_config.blockquoteFont + color:_config.blockquoteColor + headingLevel:0]; + } else { + [context setBlockStyle:BlockTypeParagraph font:_config.paragraphFont color:_config.paragraphColor headingLevel:0]; + } NSMutableAttributedString *output = [[NSMutableAttributedString alloc] init]; diff --git a/packages/react-native-enriched-markdown/ios/utils/ENRMSegmentHeightMeasurer.h b/packages/react-native-enriched-markdown/ios/utils/ENRMSegmentHeightMeasurer.h new file mode 100644 index 000000000..3781506e1 --- /dev/null +++ b/packages/react-native-enriched-markdown/ios/utils/ENRMSegmentHeightMeasurer.h @@ -0,0 +1,32 @@ +#pragma once + +#import "ENRMUIKit.h" +#import "ParagraphStyleUtils.h" +#import "RenderedMarkdownSegment.h" +#import "StyleConfig.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * View-free height summation for a vertical stack of already-rendered + * segments at a fixed content width. This is the iOS counterpart of Android's + * SegmentHeightMeasurer: it single-sources the per-segment height arithmetic so + * the shadow-node measurement pass (ENRMViewFreeMeasurement) and the blockquote + * container's own laid-out/static heights cannot drift. + * + * Each kind is measured with the same helper the visible view uses: text via a + * fresh call-confined TextKit stack (ENRMMeasureAttributedTextViewFree), tables + * via +[TableContainerView measureHeightForTableNode:...], code blocks via + * +[ENRMCodeBlockContainerView measureHeightForCodeBlockNode:...], math via + * +[ENRMMathContainerView measureHeightForLatex:...], and nested blockquotes via + * +[ENRMBlockquoteContainerView measureHeightForBlockquoteNode:...] (recursion). + * Per-kind margins come from the config exactly as computeSegmentLayout applies + * them: marginTop before, marginBottom after unless it is the last segment and + * trailing margins are disabled. + */ +CGFloat ENRMMeasureSegmentsHeightViewFree(NSArray *segments, StyleConfig *config, + CGFloat contentWidth, BOOL allowTrailingMargin, CGFloat pointScaleFactor, + ENRMWritingDirectionMode writingDirectionMode, + NSWritingDirection resolvedLayoutDirection); + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native-enriched-markdown/ios/utils/ENRMSegmentHeightMeasurer.mm b/packages/react-native-enriched-markdown/ios/utils/ENRMSegmentHeightMeasurer.mm new file mode 100644 index 000000000..11910bce6 --- /dev/null +++ b/packages/react-native-enriched-markdown/ios/utils/ENRMSegmentHeightMeasurer.mm @@ -0,0 +1,76 @@ +#import "ENRMSegmentHeightMeasurer.h" +#import "ENRMBlockquoteContainerView.h" +#import "ENRMCodeBlockContainerView.h" +#import "ENRMFeatureFlags.h" +#import "ENRMTextRenderer.h" +#import "ENRMViewFreeMeasurement.h" +#import "TableContainerView.h" +#if ENRICHED_MARKDOWN_MATH +#import "ENRMMathContainerView.h" +#endif + +CGFloat ENRMMeasureSegmentsHeightViewFree(NSArray *segments, StyleConfig *config, + CGFloat contentWidth, BOOL allowTrailingMargin, CGFloat pointScaleFactor, + ENRMWritingDirectionMode writingDirectionMode, + NSWritingDirection resolvedLayoutDirection) +{ + if (segments.count == 0) { + return 0; + } + + CGFloat totalHeight = 0; + const NSUInteger lastIndex = segments.count - 1; + + for (NSUInteger i = 0; i < segments.count; i++) { + ENRMRenderedSegment *segment = segments[i]; + const BOOL isLast = (i == lastIndex); + const BOOL shouldAddBottomMargin = (!isLast || allowTrailingMargin); + + if (segment.kind == ENRMSegmentKindText && segment.textResult) { + CGSize textSize = ENRMMeasureAttributedTextViewFree(segment.textResult.attributedText, contentWidth, config, + shouldAddBottomMargin, + segment.textResult.lastElementMarginBottom, pointScaleFactor); + totalHeight += textSize.height; + } else if (segment.kind == ENRMSegmentKindTable && segment.tableSegment) { + totalHeight += config.tableMarginTop; + totalHeight += [TableContainerView measureHeightForTableNode:segment.tableSegment.tableNode + config:config + allowFontScaling:YES + maxFontSizeMultiplier:config.maxFontSizeMultiplier + writingDirectionMode:writingDirectionMode + resolvedLayoutDirection:resolvedLayoutDirection]; + if (shouldAddBottomMargin) { + totalHeight += config.tableMarginBottom; + } + } else if (segment.kind == ENRMSegmentKindCodeBlock && segment.codeBlockSegment) { + totalHeight += config.codeBlockMarginTop; + totalHeight += [ENRMCodeBlockContainerView measureHeightForCodeBlockNode:segment.codeBlockSegment.codeBlockNode + config:config]; + if (shouldAddBottomMargin) { + totalHeight += config.codeBlockMarginBottom; + } + } else if (segment.kind == ENRMSegmentKindBlockquote && segment.blockquoteSegment) { + // This helper only ever sums a quote's own children, so any quote here is + // nested and carries no vertical margin (only the outermost quote does, + // added by the root measurement pass); mirrors ENRMBlockquoteContainerView.nested. + totalHeight += + [ENRMBlockquoteContainerView measureHeightForBlockquoteNode:segment.blockquoteSegment.blockquoteNode + config:config + maxWidth:contentWidth + pointScaleFactor:pointScaleFactor]; + } +#if ENRICHED_MARKDOWN_MATH + else if (segment.kind == ENRMSegmentKindMath && segment.mathSegment) { + totalHeight += config.mathMarginTop; + totalHeight += [ENRMMathContainerView measureHeightForLatex:segment.mathSegment.latex + config:config + maxWidth:contentWidth]; + if (shouldAddBottomMargin) { + totalHeight += config.mathMarginBottom; + } + } +#endif + } + + return totalHeight; +} diff --git a/packages/react-native-enriched-markdown/ios/utils/ENRMTextRenderer.h b/packages/react-native-enriched-markdown/ios/utils/ENRMTextRenderer.h index 092876f89..b78f207b6 100644 --- a/packages/react-native-enriched-markdown/ios/utils/ENRMTextRenderer.h +++ b/packages/react-native-enriched-markdown/ios/utils/ENRMTextRenderer.h @@ -23,6 +23,14 @@ ENRMRenderResult *ENRMRenderASTNodes(NSArray *nodes, StyleCon BOOL allowFontScaling, CGFloat maxFontSizeMultiplier, NSLineBreakStrategy lineBreakStrategy); +// Renders a blockquote's own text content: like ENRMRenderASTNodes but with a blockquote baseline +// (tight paragraphs, quote font/color) and the quote's line height applied over the result. Draws +// no box - the ENRMBlockquoteContainerView draws the border/background/padding. Mirrors Android's +// Renderer.renderBlockquoteContent. +ENRMRenderResult *ENRMRenderBlockquoteContentNodes(NSArray *nodes, StyleConfig *config, + BOOL allowFontScaling, CGFloat maxFontSizeMultiplier, + NSLineBreakStrategy lineBreakStrategy); + #ifdef __cplusplus } #endif diff --git a/packages/react-native-enriched-markdown/ios/utils/ENRMTextRenderer.m b/packages/react-native-enriched-markdown/ios/utils/ENRMTextRenderer.m index 8e7a26866..49a175437 100644 --- a/packages/react-native-enriched-markdown/ios/utils/ENRMTextRenderer.m +++ b/packages/react-native-enriched-markdown/ios/utils/ENRMTextRenderer.m @@ -9,9 +9,10 @@ @implementation ENRMRenderResult @end -ENRMRenderResult *ENRMRenderASTNodes(NSArray *nodes, StyleConfig *config, BOOL allowTrailingMargin, - BOOL allowFontScaling, CGFloat maxFontSizeMultiplier, - NSLineBreakStrategy lineBreakStrategy) +static ENRMRenderResult *ENRMRenderASTNodesCore(NSArray *nodes, StyleConfig *config, + BOOL allowTrailingMargin, BOOL allowFontScaling, + CGFloat maxFontSizeMultiplier, NSLineBreakStrategy lineBreakStrategy, + BOOL asBlockquoteContent) { MarkdownASTNode *root = [[MarkdownASTNode alloc] initWithType:MarkdownNodeTypeDocument]; for (MarkdownASTNode *node in nodes) { @@ -25,7 +26,16 @@ @implementation ENRMRenderResult context.allowFontScaling = allowFontScaling; context.maxFontSizeMultiplier = maxFontSizeMultiplier; - NSMutableAttributedString *attributedText = [renderer renderRoot:root context:context]; + NSMutableAttributedString *attributedText = [renderer renderRoot:root + context:context + asBlockquoteContent:asBlockquoteContent]; + + // Paragraphs apply the paragraph line height regardless of block type; a quote's own line + // height is stamped over the whole run here, matching Renderer.renderBlockquoteContent on Android. + if (asBlockquoteContent && attributedText.length > 0) { + applyLineHeight(attributedText, NSMakeRange(0, attributedText.length), [config blockquoteLineHeight]); + } + [context applyLinkAttributesToString:attributedText]; ENRMApplyLineBreakStrategyToParagraphStyles(attributedText, lineBreakStrategy); @@ -36,3 +46,19 @@ @implementation ENRMRenderResult result.lastElementMarginBottom = [renderer getLastElementMarginBottom]; return result; } + +ENRMRenderResult *ENRMRenderASTNodes(NSArray *nodes, StyleConfig *config, BOOL allowTrailingMargin, + BOOL allowFontScaling, CGFloat maxFontSizeMultiplier, + NSLineBreakStrategy lineBreakStrategy) +{ + return ENRMRenderASTNodesCore(nodes, config, allowTrailingMargin, allowFontScaling, maxFontSizeMultiplier, + lineBreakStrategy, /*asBlockquoteContent*/ NO); +} + +ENRMRenderResult *ENRMRenderBlockquoteContentNodes(NSArray *nodes, StyleConfig *config, + BOOL allowFontScaling, CGFloat maxFontSizeMultiplier, + NSLineBreakStrategy lineBreakStrategy) +{ + return ENRMRenderASTNodesCore(nodes, config, /*allowTrailingMargin*/ NO, allowFontScaling, maxFontSizeMultiplier, + lineBreakStrategy, /*asBlockquoteContent*/ YES); +} diff --git a/packages/react-native-enriched-markdown/ios/utils/RenderedMarkdownSegment.h b/packages/react-native-enriched-markdown/ios/utils/RenderedMarkdownSegment.h index 84f474172..3193566ac 100644 --- a/packages/react-native-enriched-markdown/ios/utils/RenderedMarkdownSegment.h +++ b/packages/react-native-enriched-markdown/ios/utils/RenderedMarkdownSegment.h @@ -11,7 +11,8 @@ typedef NS_ENUM(NSInteger, ENRMSegmentKind) { ENRMSegmentKindText, ENRMSegmentKindTable, ENRMSegmentKindMath, - ENRMSegmentKindCodeBlock + ENRMSegmentKindCodeBlock, + ENRMSegmentKindBlockquote }; @interface ENRMTextSegment : NSObject @@ -34,6 +35,11 @@ typedef NS_ENUM(NSInteger, ENRMSegmentKind) { + (instancetype)segmentWithCodeBlockNode:(MarkdownASTNode *)node; @end +@interface ENRMBlockquoteSegment : NSObject +@property (nonatomic, strong) MarkdownASTNode *blockquoteNode; ++ (instancetype)segmentWithBlockquoteNode:(MarkdownASTNode *)node; +@end + @interface ENRMRenderedSegment : NSObject @property (nonatomic, assign) ENRMSegmentKind kind; @property (nonatomic, assign) uint64_t signature; @@ -41,10 +47,12 @@ typedef NS_ENUM(NSInteger, ENRMSegmentKind) { @property (nonatomic, strong, nullable) ENRMTableSegment *tableSegment; @property (nonatomic, strong, nullable) ENRMMathSegment *mathSegment; @property (nonatomic, strong, nullable) ENRMCodeBlockSegment *codeBlockSegment; +@property (nonatomic, strong, nullable) ENRMBlockquoteSegment *blockquoteSegment; + (instancetype)textSegmentWithResult:(ENRMRenderResult *)result signature:(uint64_t)signature; + (instancetype)tableSegmentWithSegment:(ENRMTableSegment *)segment signature:(uint64_t)signature; + (instancetype)mathSegmentWithSegment:(ENRMMathSegment *)segment signature:(uint64_t)signature; + (instancetype)codeBlockSegmentWithSegment:(ENRMCodeBlockSegment *)segment signature:(uint64_t)signature; ++ (instancetype)blockquoteSegmentWithSegment:(ENRMBlockquoteSegment *)segment signature:(uint64_t)signature; @end #ifdef __cplusplus diff --git a/packages/react-native-enriched-markdown/ios/utils/RenderedMarkdownSegment.m b/packages/react-native-enriched-markdown/ios/utils/RenderedMarkdownSegment.m index f2a0a5197..27663c08b 100644 --- a/packages/react-native-enriched-markdown/ios/utils/RenderedMarkdownSegment.m +++ b/packages/react-native-enriched-markdown/ios/utils/RenderedMarkdownSegment.m @@ -41,6 +41,16 @@ + (instancetype)segmentWithCodeBlockNode:(MarkdownASTNode *)node } @end +@implementation ENRMBlockquoteSegment ++ (instancetype)segmentWithBlockquoteNode:(MarkdownASTNode *)node +{ + NSParameterAssert(node != nil); + ENRMBlockquoteSegment *segment = [[ENRMBlockquoteSegment alloc] init]; + segment.blockquoteNode = node; + return segment; +} +@end + @implementation ENRMRenderedSegment + (instancetype)textSegmentWithResult:(ENRMRenderResult *)result signature:(uint64_t)signature { @@ -81,6 +91,16 @@ + (instancetype)codeBlockSegmentWithSegment:(ENRMCodeBlockSegment *)codeBlockSeg segment.signature = signature; return segment; } + ++ (instancetype)blockquoteSegmentWithSegment:(ENRMBlockquoteSegment *)blockquoteSegment signature:(uint64_t)signature +{ + NSParameterAssert(blockquoteSegment != nil); + ENRMRenderedSegment *segment = [[ENRMRenderedSegment alloc] init]; + segment.kind = ENRMSegmentKindBlockquote; + segment.blockquoteSegment = blockquoteSegment; + segment.signature = signature; + return segment; +} @end // FNV-1a 64-bit hash for segment signatures. Collisions are theoretically diff --git a/packages/react-native-enriched-markdown/ios/utils/SegmentRenderer.h b/packages/react-native-enriched-markdown/ios/utils/SegmentRenderer.h index d598ff2bf..15831911d 100644 --- a/packages/react-native-enriched-markdown/ios/utils/SegmentRenderer.h +++ b/packages/react-native-enriched-markdown/ios/utils/SegmentRenderer.h @@ -12,10 +12,12 @@ NS_ASSUME_NONNULL_BEGIN extern "C" { #endif -NSArray *ENRMRenderSegmentsFromAST(MarkdownASTNode *ast, StyleConfig *config, - BOOL allowTrailingMargin, BOOL allowFontScaling, - CGFloat maxFontSizeMultiplier, - NSLineBreakStrategy lineBreakStrategy); +// blockquoteContent = YES when splitting a blockquote's own children (called by +// ENRMBlockquoteContainerView): text segments are then rendered with blockquote styling +// (tight paragraphs, quote font/color/line height) instead of paragraph styling. +NSArray * +ENRMRenderSegmentsFromAST(MarkdownASTNode *ast, StyleConfig *config, BOOL allowTrailingMargin, BOOL allowFontScaling, + CGFloat maxFontSizeMultiplier, NSLineBreakStrategy lineBreakStrategy, BOOL blockquoteContent); #ifdef __cplusplus } diff --git a/packages/react-native-enriched-markdown/ios/utils/SegmentRenderer.m b/packages/react-native-enriched-markdown/ios/utils/SegmentRenderer.m index 64defe004..06f100e8d 100644 --- a/packages/react-native-enriched-markdown/ios/utils/SegmentRenderer.m +++ b/packages/react-native-enriched-markdown/ios/utils/SegmentRenderer.m @@ -39,6 +39,12 @@ [currentTextNodes removeAllObjects]; } [segments addObject:[ENRMCodeBlockSegment segmentWithCodeBlockNode:child]]; + } else if (child.type == MarkdownNodeTypeBlockquote) { + if (currentTextNodes.count > 0) { + [segments addObject:[ENRMTextSegment segmentWithNodes:[currentTextNodes copy]]]; + [currentTextNodes removeAllObjects]; + } + [segments addObject:[ENRMBlockquoteSegment segmentWithBlockquoteNode:child]]; } else { [currentTextNodes addObject:child]; } @@ -54,21 +60,25 @@ NSArray *ENRMRenderSegmentsFromAST(MarkdownASTNode *ast, StyleConfig *config, BOOL allowTrailingMargin, BOOL allowFontScaling, CGFloat maxFontSizeMultiplier, - NSLineBreakStrategy lineBreakStrategy) + NSLineBreakStrategy lineBreakStrategy, BOOL blockquoteContent) { NSArray *segments = ENRMSplitASTIntoSegments(ast); NSMutableArray *renderedSegments = [NSMutableArray array]; - static const uint64_t kTextKindSalt = 0x7465787400000000ULL; // "text" - static const uint64_t kTableKindSalt = 0x7461626C00000000ULL; // "tabl" - static const uint64_t kMathKindSalt = 0x6D61746800000000ULL; // "math" - static const uint64_t kCodeBlockKindSalt = 0x63626C6B00000000ULL; // "cblk" + static const uint64_t kTextKindSalt = 0x7465787400000000ULL; // "text" + static const uint64_t kTableKindSalt = 0x7461626C00000000ULL; // "tabl" + static const uint64_t kMathKindSalt = 0x6D61746800000000ULL; // "math" + static const uint64_t kCodeBlockKindSalt = 0x63626C6B00000000ULL; // "cblk" + static const uint64_t kBlockquoteKindSalt = 0x6271746500000000ULL; // "bqte" for (id segment in segments) { if ([segment isKindOfClass:[ENRMTextSegment class]]) { ENRMTextSegment *textSegment = (ENRMTextSegment *)segment; - ENRMRenderResult *rendered = ENRMRenderASTNodes(textSegment.nodes, config, allowTrailingMargin, allowFontScaling, - maxFontSizeMultiplier, lineBreakStrategy); + ENRMRenderResult *rendered = blockquoteContent + ? ENRMRenderBlockquoteContentNodes(textSegment.nodes, config, allowFontScaling, + maxFontSizeMultiplier, lineBreakStrategy) + : ENRMRenderASTNodes(textSegment.nodes, config, allowTrailingMargin, + allowFontScaling, maxFontSizeMultiplier, lineBreakStrategy); uint64_t signature = ENRMSignatureForNodes(textSegment.nodes) ^ kTextKindSalt; [renderedSegments addObject:[ENRMRenderedSegment textSegmentWithResult:rendered signature:signature]]; } else if ([segment isKindOfClass:[ENRMTableSegment class]]) { @@ -94,6 +104,11 @@ uint64_t signature = ENRMSignatureForNode(codeBlockSegment.codeBlockNode) ^ kCodeBlockKindSalt; [renderedSegments addObject:[ENRMRenderedSegment codeBlockSegmentWithSegment:codeBlockSegment signature:signature]]; + } else if ([segment isKindOfClass:[ENRMBlockquoteSegment class]]) { + ENRMBlockquoteSegment *blockquoteSegment = (ENRMBlockquoteSegment *)segment; + uint64_t signature = ENRMSignatureForNode(blockquoteSegment.blockquoteNode) ^ kBlockquoteKindSalt; + [renderedSegments addObject:[ENRMRenderedSegment blockquoteSegmentWithSegment:blockquoteSegment + signature:signature]]; } } diff --git a/packages/react-native-enriched-markdown/ios/views/ENRMBlockquoteContainerView.h b/packages/react-native-enriched-markdown/ios/views/ENRMBlockquoteContainerView.h new file mode 100644 index 000000000..7670d3532 --- /dev/null +++ b/packages/react-native-enriched-markdown/ios/views/ENRMBlockquoteContainerView.h @@ -0,0 +1,66 @@ +#pragma once + +#import "ENRMContainerNodeView.h" +#import "StyleConfig.h" + +@class MarkdownASTNode; + +NS_ASSUME_NONNULL_BEGIN + +typedef void (^ENRMBlockquoteLinkBlock)(NSString *url); + +/** + * A GFM blockquote rendered as a recursive container (issue #608): it splits its + * own AST children into segments and lays them out through the shared + * ENRMContainerNodeView machinery, so a nested quote becomes another + * ENRMBlockquoteContainerView, a fenced code block becomes an + * ENRMCodeBlockContainerView, and prose collapses into a single inner text view. + * Nesting depth is automatic: each level is its own view, inset further by its + * padding, so drawRect: only ever renders one accent bar plus the background for + * this single level. This mirrors Android's BlockquoteContainerView. + * + * The content inset is applied via contentInsets (left = borderWidth + gapWidth + * + padding, other edges = padding), reusing the geometry the commonmark + * BlockquoteBorder draws for the in-text (list-nested) path, which stays + * untouched. The base measure/layout already honor contentInsets. + * + * Streaming is static inside a quote (no pending fence, no animation); a content + * change re-signs the whole subtree so the outer reconciler still reuses the + * quote view. + */ +@interface ENRMBlockquoteContainerView : ENRMContainerNodeView + +- (instancetype)initWithConfig:(StyleConfig *)config; + +// Splits the node's children into segments and applies them to this container. +- (void)applyBlockquoteNode:(MarkdownASTNode *)node; + +// Laid-out height of this quote at the given outer width (the width the parent +// lays it out at, before this view's own inset). +- (CGFloat)measureHeight:(CGFloat)maxWidth; + +// View-free height for the shadow-node measurement pass: the same height an +// instance's measureHeight: reports for the node, without building a view. It +// sums the child segments at the reduced inner width and adds the vertical +// inset, recursing into itself for nested quotes. pointScaleFactor is the layout +// context's scale (the visible instance path uses RCTScreenScale), threaded so +// the per-text-segment pixel rounding matches the laid-out height. ++ (CGFloat)measureHeightForBlockquoteNode:(MarkdownASTNode *)node + config:(StyleConfig *)config + maxWidth:(CGFloat)maxWidth + pointScaleFactor:(CGFloat)pointScaleFactor; + +// YES when this quote is nested directly inside another quote. A nested quote +// carries no vertical margin (only the outermost quote does, applied by the root +// host), matching the commonmark path which applies blockquote margins only at +// depth 0. Set by the parent quote's child registry. +@property (nonatomic, assign) BOOL nested; + +// Bridged up to the JS onLinkPress/onLinkLongPress events; propagated to nested +// quotes so links at any depth fire. +@property (nonatomic, copy, nullable) ENRMBlockquoteLinkBlock onLinkPress; +@property (nonatomic, copy, nullable) ENRMBlockquoteLinkBlock onLinkLongPress; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native-enriched-markdown/ios/views/ENRMBlockquoteContainerView.mm b/packages/react-native-enriched-markdown/ios/views/ENRMBlockquoteContainerView.mm new file mode 100644 index 000000000..cbcd4a4e3 --- /dev/null +++ b/packages/react-native-enriched-markdown/ios/views/ENRMBlockquoteContainerView.mm @@ -0,0 +1,262 @@ +#import "ENRMBlockquoteContainerView.h" +#import "ENRMCodeBlockContainerView.h" +#import "ENRMFeatureFlags.h" +#import "ENRMSegmentHeightMeasurer.h" +#import "ENRMTextInteractionUtils.h" +#import "ENRMTextRenderer.h" +#import "EnrichedMarkdownInternalText.h" +#import "MarkdownASTNode.h" +#import "SegmentRenderer.h" +#import "TableContainerView.h" +#if ENRICHED_MARKDOWN_MATH +#import "ENRMMathContainerView.h" +#endif + +// Inner content is rendered with trailing margins disabled and the default line +// break strategy; the quote's own vertical inset provides the bottom gap, and +// per-segment margins between children still apply (allowTrailingMargin only +// gates the last child's bottom margin). +static NSArray *ENRMRenderBlockquoteChildren(MarkdownASTNode *node, StyleConfig *config) +{ + return ENRMRenderSegmentsFromAST(node, config, /*allowTrailingMargin*/ NO, /*allowFontScaling*/ YES, + config.maxFontSizeMultiplier, NSLineBreakStrategyNone, /*blockquoteContent*/ YES); +} + +static UIEdgeInsets ENRMBlockquoteContentInsets(StyleConfig *config) +{ + CGFloat padding = config.blockquotePadding; + CGFloat left = ceil(config.blockquoteBorderWidth + config.blockquoteGapWidth + padding); + CGFloat vertical = ceil(padding); + CGFloat right = ceil(padding); + return UIEdgeInsetsMake(vertical, left, vertical, right); +} + +@implementation ENRMBlockquoteContainerView + +- (instancetype)initWithConfig:(StyleConfig *)config +{ + self = [super initWithFrame:CGRectZero]; + if (self) { + self.config = config; + self.contentInsets = ENRMBlockquoteContentInsets(config); + self.allowTrailingMargin = NO; +#if !TARGET_OS_OSX + self.contentMode = UIViewContentModeRedraw; +#endif + self.segmentViewRegistry = [self buildChildRegistry]; + } + return self; +} + +- (void)applyBlockquoteNode:(MarkdownASTNode *)node +{ + NSArray *rendered = ENRMRenderBlockquoteChildren(node, self.config); + [self applySegments:rendered reset:NO]; +} + +// Child registry for this quote's own content. It reuses static creators for +// every kind and, for a nested Blockquote, creates another +// ENRMBlockquoteContainerView (the recursion). Streaming is static: no +// animation, no pending-fence sync. +- (ENRMSegmentViewRegistry *)buildChildRegistry +{ + StyleConfig *config = self.config; + __weak ENRMBlockquoteContainerView *weakSelf = self; + NSMutableArray *handlers = [NSMutableArray array]; + + [handlers + addObject:[ENRMSegmentViewHandler handlerWithKind:ENRMSegmentKindText + matchesView:^BOOL(RCTUIView *view, ENRMRenderedSegment *segment) { + return [view isKindOfClass:[EnrichedMarkdownInternalText class]]; + } + createView:^RCTUIView *(ENRMRenderedSegment *segment) { + ENRMBlockquoteContainerView *strongSelf = weakSelf; + EnrichedMarkdownInternalText *view = [[EnrichedMarkdownInternalText alloc] initWithConfig:config]; + view.lastElementMarginBottom = segment.textResult.lastElementMarginBottom; + view.accessibilityInfo = segment.textResult.accessibilityInfo; + [view applyAttributedText:segment.textResult.attributedText context:segment.textResult.context]; + if (strongSelf) { + [strongSelf attachLinkTapToTextView:view]; + } + return view; + } + updateView:^(RCTUIView *view, ENRMRenderedSegment *segment) { + EnrichedMarkdownInternalText *textView = (EnrichedMarkdownInternalText *)view; + textView.lastElementMarginBottom = segment.textResult.lastElementMarginBottom; + textView.accessibilityInfo = segment.textResult.accessibilityInfo; + [textView applyAttributedText:segment.textResult.attributedText + context:segment.textResult.context]; + }]]; + + [handlers addObject:[ENRMSegmentViewHandler handlerWithKind:ENRMSegmentKindTable + matchesView:^BOOL(RCTUIView *view, ENRMRenderedSegment *segment) { + return [view isKindOfClass:[TableContainerView class]]; + } + createView:^RCTUIView *(ENRMRenderedSegment *segment) { + TableContainerView *view = [[TableContainerView alloc] initWithConfig:config]; + ENRMBlockquoteContainerView *strongSelf = weakSelf; + if (strongSelf) { + view.onLinkPress = ^(NSString *url) { + ENRMBlockquoteContainerView *s = weakSelf; + if (s.onLinkPress && url) + s.onLinkPress(url); + }; + view.onLinkLongPress = ^(NSString *url) { + ENRMBlockquoteContainerView *s = weakSelf; + if (s.onLinkLongPress && url) + s.onLinkLongPress(url); + }; + } + [view applyTableNode:segment.tableSegment.tableNode]; + return view; + } + updateView:^(RCTUIView *view, ENRMRenderedSegment *segment) { + [(TableContainerView *)view applyTableNode:segment.tableSegment.tableNode]; + }]]; + + [handlers + addObject:[ENRMSegmentViewHandler handlerWithKind:ENRMSegmentKindCodeBlock + matchesView:^BOOL(RCTUIView *view, ENRMRenderedSegment *segment) { + return [view isKindOfClass:[ENRMCodeBlockContainerView class]]; + } + createView:^RCTUIView *(ENRMRenderedSegment *segment) { + ENRMCodeBlockContainerView *view = [[ENRMCodeBlockContainerView alloc] initWithConfig:config]; + [view applyCodeBlockNode:segment.codeBlockSegment.codeBlockNode]; + return view; + } + updateView:^(RCTUIView *view, ENRMRenderedSegment *segment) { + [(ENRMCodeBlockContainerView *)view applyCodeBlockNode:segment.codeBlockSegment.codeBlockNode]; + }]]; + + [handlers addObject:[ENRMSegmentViewHandler handlerWithKind:ENRMSegmentKindBlockquote + matchesView:^BOOL(RCTUIView *view, ENRMRenderedSegment *segment) { + return [view isKindOfClass:[ENRMBlockquoteContainerView class]]; + } + createView:^RCTUIView *(ENRMRenderedSegment *segment) { + ENRMBlockquoteContainerView *view = + [[ENRMBlockquoteContainerView alloc] initWithConfig:config]; + view.nested = YES; + ENRMBlockquoteContainerView *strongSelf = weakSelf; + if (strongSelf) { + view.onLinkPress = ^(NSString *url) { + ENRMBlockquoteContainerView *s = weakSelf; + if (s.onLinkPress && url) + s.onLinkPress(url); + }; + view.onLinkLongPress = ^(NSString *url) { + ENRMBlockquoteContainerView *s = weakSelf; + if (s.onLinkLongPress && url) + s.onLinkLongPress(url); + }; + } + [view applyBlockquoteNode:segment.blockquoteSegment.blockquoteNode]; + return view; + } + updateView:^(RCTUIView *view, ENRMRenderedSegment *segment) { + [(ENRMBlockquoteContainerView *)view + applyBlockquoteNode:segment.blockquoteSegment.blockquoteNode]; + }]]; + +#if ENRICHED_MARKDOWN_MATH +#if !TARGET_OS_OSX + [handlers addObject:[ENRMSegmentViewHandler handlerWithKind:ENRMSegmentKindMath + matchesView:^BOOL(RCTUIView *view, ENRMRenderedSegment *segment) { + return [view isKindOfClass:[ENRMMathContainerView class]]; + } + createView:^RCTUIView *(ENRMRenderedSegment *segment) { + ENRMMathContainerView *view = [[ENRMMathContainerView alloc] initWithConfig:config]; + [view applyLatex:segment.mathSegment.latex]; + return view; + } + updateView:^(RCTUIView *view, ENRMRenderedSegment *segment) { + [(ENRMMathContainerView *)view applyLatex:segment.mathSegment.latex]; + }]]; +#endif +#endif + + return [[ENRMSegmentViewRegistry alloc] initWithHandlers:handlers]; +} + +- (void)attachLinkTapToTextView:(EnrichedMarkdownInternalText *)view +{ + ENRMTapRecognizer *tap = [[ENRMTapRecognizer alloc] initWithTarget:self action:@selector(handleTextTap:)]; + [view.textView addGestureRecognizer:tap]; +} + +- (void)handleTextTap:(ENRMTapRecognizer *)recognizer +{ + ENRMPlatformTextView *textView = (ENRMPlatformTextView *)recognizer.view; + __weak ENRMBlockquoteContainerView *weakSelf = self; + ENRMHandleTapOnTextView(textView, recognizer, ^(NSString *url) { + ENRMBlockquoteContainerView *strongSelf = weakSelf; + if (strongSelf.onLinkPress && url) + strongSelf.onLinkPress(url); + }); +} + +- (CGFloat)measureHeight:(CGFloat)maxWidth +{ + return [self computeContentHeightForWidth:MAX(maxWidth - self.contentInsets.left - self.contentInsets.right, 0)]; +} + ++ (CGFloat)measureHeightForBlockquoteNode:(MarkdownASTNode *)node + config:(StyleConfig *)config + maxWidth:(CGFloat)maxWidth + pointScaleFactor:(CGFloat)pointScaleFactor +{ + UIEdgeInsets insets = ENRMBlockquoteContentInsets(config); + CGFloat innerWidth = MAX(maxWidth - insets.left - insets.right, 1); + + NSArray *rendered = ENRMRenderBlockquoteChildren(node, config); + CGFloat childrenHeight = + ENRMMeasureSegmentsHeightViewFree(rendered, config, innerWidth, /*allowTrailingMargin*/ NO, pointScaleFactor, + ENRMWritingDirectionModeFirstStrong, NSWritingDirectionNatural); + + return childrenHeight + insets.top + insets.bottom; +} + +- (void)drawRect:(CGRect)rect +{ + StyleConfig *config = self.config; + CGFloat borderWidth = config.blockquoteBorderWidth; + CGFloat radius = config.blockquoteBorderRadius; + CGRect bounds = self.bounds; + + // The rounded (or plain) box outline, shared by the background fill and the accent + // bar's clip so the bar's left corners follow the border radius. +#if !TARGET_OS_OSX + UIBezierPath *boxPath = radius > 0 ? [UIBezierPath bezierPathWithRoundedRect:bounds cornerRadius:radius] + : [UIBezierPath bezierPathWithRect:bounds]; +#else + NSBezierPath *boxPath = radius > 0 ? [NSBezierPath bezierPathWithRoundedRect:bounds xRadius:radius yRadius:radius] + : [NSBezierPath bezierPathWithRect:bounds]; +#endif + + RCTUIColor *backgroundColor = config.blockquoteBackgroundColor; + if (backgroundColor && backgroundColor != [RCTUIColor clearColor]) { + [backgroundColor setFill]; + [boxPath fill]; + } + + if (borderWidth > 0) { + RCTUIColor *borderColor = config.blockquoteBorderColor; + if (borderColor) { + CGRect barRect = CGRectMake(0, 0, borderWidth, bounds.size.height); + [borderColor setFill]; +#if !TARGET_OS_OSX + CGContextRef ctx = UIGraphicsGetCurrentContext(); + CGContextSaveGState(ctx); + [boxPath addClip]; + [[UIBezierPath bezierPathWithRect:barRect] fill]; + CGContextRestoreGState(ctx); +#else + [NSGraphicsContext saveGraphicsState]; + [boxPath addClip]; + [[NSBezierPath bezierPathWithRect:barRect] fill]; + [NSGraphicsContext restoreGraphicsState]; +#endif + } + } +} + +@end diff --git a/packages/react-native-enriched-markdown/ios/views/ENRMContainerNodeView.h b/packages/react-native-enriched-markdown/ios/views/ENRMContainerNodeView.h new file mode 100644 index 000000000..41e522d05 --- /dev/null +++ b/packages/react-native-enriched-markdown/ios/views/ENRMContainerNodeView.h @@ -0,0 +1,52 @@ +#pragma once + +#import "ENRMUIKit.h" +#import "RenderedMarkdownSegment.h" +#import "SegmentViewRegistry.h" +#import "StyleConfig.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Reusable host view for an AST branch node that holds a vertical stack of block + * children (currently every blockquote; the analogue of Android's + * ContainerNodeView). It owns the machinery shared by such hosts: reconciling a + * list of ENRMRenderedSegment into real child views by signature via + * ENRMSegmentReconciler + a per-kind ENRMSegmentViewRegistry the subclass + * supplies, stacking them vertically with per-segment margins, and reporting the + * total height (including its own content insets). + * + * Layout honors contentInsets so a subclass can inset its children (e.g. a + * blockquote's border/gap/padding): children are laid out at width minus the + * horizontal insets, starting at (insets.left, insets.top). Per-kind margins are + * read from the config exactly as the root view's layout applies them. + * + * Streaming inside a container is treated as static: no fade/tail animation and + * no pending-code-fence sync. A content change re-signs the whole subtree, so + * the outer reconciler still reuses the container view by signature. + */ +@interface ENRMContainerNodeView : RCTUIView + +@property (nonatomic, strong) StyleConfig *config; + +// Set by a subclass before applySegments:reset:. The reconcile/layout loop is +// identical across hosts; only the child view construction varies per registry. +@property (nonatomic, strong) ENRMSegmentViewRegistry *segmentViewRegistry; + +// Inset applied to all children. A plain container leaves this zero. +@property (nonatomic, assign) UIEdgeInsets contentInsets; + +// Whether the last child contributes its trailing bottom margin. +@property (nonatomic, assign) BOOL allowTrailingMargin; + +// Reconciles the current child views against renderedSegments, attaching and +// removing subviews as needed. Requests a re-layout. +- (void)applySegments:(NSArray *)renderedSegments reset:(BOOL)reset; + +// Total laid-out height for the current children at contentWidth (inner width, +// i.e. bounds width minus horizontal insets), including the vertical insets. +- (CGFloat)computeContentHeightForWidth:(CGFloat)contentWidth; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native-enriched-markdown/ios/views/ENRMContainerNodeView.mm b/packages/react-native-enriched-markdown/ios/views/ENRMContainerNodeView.mm new file mode 100644 index 000000000..53fc6f7d9 --- /dev/null +++ b/packages/react-native-enriched-markdown/ios/views/ENRMContainerNodeView.mm @@ -0,0 +1,183 @@ +#import "ENRMContainerNodeView.h" +#import "ENRMBlockquoteContainerView.h" +#import "ENRMCodeBlockContainerView.h" +#import "ENRMFeatureFlags.h" +#import "EnrichedMarkdownInternalText.h" +#import "SegmentReconciler.h" +#import "TableContainerView.h" +#if ENRICHED_MARKDOWN_MATH +#import "ENRMMathContainerView.h" +#endif + +@implementation ENRMContainerNodeView { + NSMutableArray *_segmentViews; + NSMutableArray *_segmentSignatures; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + _segmentViews = [NSMutableArray array]; + _segmentSignatures = [NSMutableArray array]; + _contentInsets = UIEdgeInsetsZero; + _allowTrailingMargin = NO; + self.backgroundColor = [RCTUIColor clearColor]; +#if !TARGET_OS_OSX + self.contentMode = UIViewContentModeRedraw; +#endif + } + return self; +} + +#if TARGET_OS_OSX +- (BOOL)isFlipped +{ + return YES; +} +#endif + +- (void)applySegments:(NSArray *)renderedSegments reset:(BOOL)reset +{ + ENRMSegmentViewRegistry *registry = _segmentViewRegistry; + ENRMSegmentReconciliationResult *result = [ENRMSegmentReconciler reconcileCurrentViews:_segmentViews + currentSignatures:_segmentSignatures + renderedSegments:renderedSegments + reset:reset + createView:^RCTUIView *(ENRMRenderedSegment *segment) { return [registry createViewForSegment:segment]; } + updateView:^(RCTUIView *view, ENRMRenderedSegment *segment) { [registry updateView:view withSegment:segment]; } + attachView:^(RCTUIView *view) { [self addSubview:view]; } + removeView:^(RCTUIView *view) { [view removeFromSuperview]; } + matchesKind:^BOOL(RCTUIView *view, ENRMRenderedSegment *segment) { + return [registry view:view matchesSegment:segment]; + }]; + + _segmentViews = result.views; + _segmentSignatures = result.signatures; + +#if !TARGET_OS_OSX + [self setNeedsLayout]; +#else + self.needsLayout = YES; +#endif +} + +- (CGFloat)marginTopForView:(RCTUIView *)view +{ + if ([view isKindOfClass:[TableContainerView class]]) { + return _config.tableMarginTop; + } + if ([view isKindOfClass:[ENRMCodeBlockContainerView class]]) { + return _config.codeBlockMarginTop; + } + if ([view isKindOfClass:[ENRMBlockquoteContainerView class]]) { + // A quote laid out inside another quote is nested and carries no margin; the + // outermost quote's margin is applied by the root host instead. + return ((ENRMBlockquoteContainerView *)view).nested ? 0 : _config.blockquoteMarginTop; + } +#if ENRICHED_MARKDOWN_MATH + if ([view isKindOfClass:[ENRMMathContainerView class]]) { + return _config.mathMarginTop; + } +#endif + return 0; +} + +- (CGFloat)marginBottomForView:(RCTUIView *)view +{ + if ([view isKindOfClass:[TableContainerView class]]) { + return _config.tableMarginBottom; + } + if ([view isKindOfClass:[ENRMCodeBlockContainerView class]]) { + return _config.codeBlockMarginBottom; + } + if ([view isKindOfClass:[ENRMBlockquoteContainerView class]]) { + return ((ENRMBlockquoteContainerView *)view).nested ? 0 : _config.blockquoteMarginBottom; + } +#if ENRICHED_MARKDOWN_MATH + if ([view isKindOfClass:[ENRMMathContainerView class]]) { + return _config.mathMarginBottom; + } +#endif + return 0; +} + +- (CGFloat)heightForView:(RCTUIView *)view atWidth:(CGFloat)width shouldAddBottomMargin:(BOOL)shouldAddBottomMargin +{ + if ([view isKindOfClass:[EnrichedMarkdownInternalText class]]) { + EnrichedMarkdownInternalText *textView = (EnrichedMarkdownInternalText *)view; + textView.allowTrailingMargin = shouldAddBottomMargin; + return [textView measureSize:width].height; + } + if ([view isKindOfClass:[TableContainerView class]]) { + return [(TableContainerView *)view measureHeight:width]; + } + if ([view isKindOfClass:[ENRMCodeBlockContainerView class]]) { + return [(ENRMCodeBlockContainerView *)view measureHeight:width]; + } + if ([view isKindOfClass:[ENRMBlockquoteContainerView class]]) { + return [(ENRMBlockquoteContainerView *)view measureHeight:width]; + } +#if ENRICHED_MARKDOWN_MATH + if ([view isKindOfClass:[ENRMMathContainerView class]]) { + return [(ENRMMathContainerView *)view measureHeight:width]; + } +#endif + return 0; +} + +// Mirrors the root's computeSegmentLayoutForWidth: (text via measureSize:, +// block kinds via measureHeight: plus their config margins), but confined to the +// inner content rect defined by contentInsets so a subclass can pad its children. +- (CGSize)layoutChildrenForOuterWidth:(CGFloat)outerWidth applyFrames:(BOOL)applyFrames +{ + CGFloat contentWidth = MAX(outerWidth - _contentInsets.left - _contentInsets.right, 0); + if (_segmentViews.count == 0 || contentWidth <= 0) { + return CGSizeMake(0, _contentInsets.top + _contentInsets.bottom); + } + + CGFloat yOffset = _contentInsets.top; + const NSUInteger lastIndex = _segmentViews.count - 1; + + for (NSUInteger i = 0; i < _segmentViews.count; i++) { + RCTUIView *view = _segmentViews[i]; + const BOOL isLast = (i == lastIndex); + const BOOL shouldAddBottomMargin = (!isLast || _allowTrailingMargin); + + yOffset += [self marginTopForView:view]; + CGFloat height = [self heightForView:view atWidth:contentWidth shouldAddBottomMargin:shouldAddBottomMargin]; + + if (applyFrames) { + view.frame = CGRectMake(_contentInsets.left, yOffset, contentWidth, height); + } + + yOffset += height; + if (shouldAddBottomMargin) { + yOffset += [self marginBottomForView:view]; + } + } + + yOffset += _contentInsets.bottom; + return CGSizeMake(outerWidth, yOffset); +} + +- (CGFloat)computeContentHeightForWidth:(CGFloat)contentWidth +{ + return [self layoutChildrenForOuterWidth:contentWidth + _contentInsets.left + _contentInsets.right applyFrames:NO] + .height; +} + +#if !TARGET_OS_OSX +- (void)layoutSubviews +{ + [super layoutSubviews]; + [self layoutChildrenForOuterWidth:self.bounds.size.width applyFrames:YES]; +} +#else +- (void)layout +{ + [super layout]; + [self layoutChildrenForOuterWidth:self.bounds.size.width applyFrames:YES]; +} +#endif + +@end From 3d104bd78f23853835699281674c0f8b06ad16d3 Mon Sep 17 00:00:00 2001 From: Ernest Date: Mon, 17 Aug 2026 13:54:43 +0200 Subject: [PATCH 02/11] feat: android --- .../enriched/markdown/EnrichedMarkdown.kt | 1023 +++++++---------- .../enriched/markdown/MeasurementStore.kt | 10 + .../enriched/markdown/renderer/Renderer.kt | 47 + .../markdown/utils/common/MarkdownSegment.kt | 9 + .../markdown/utils/common/RenderedSegment.kt | 26 +- .../markdown/utils/common/SegmentSignature.kt | 1 + .../markdown/views/BlockquoteContainerView.kt | 253 ++++ .../markdown/views/ContainerNodeView.kt | 155 +++ .../markdown/views/SegmentHeightMeasurer.kt | 125 ++ .../markdown/views/SegmentViewCreators.kt | 177 +++ .../markdown/views/SegmentViewFactory.kt | 30 + 11 files changed, 1277 insertions(+), 579 deletions(-) create mode 100644 packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/BlockquoteContainerView.kt create mode 100644 packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/ContainerNodeView.kt create mode 100644 packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/SegmentHeightMeasurer.kt create mode 100644 packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/SegmentViewCreators.kt create mode 100644 packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/SegmentViewFactory.kt diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt index c67dd9ddb..9bacaa728 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt @@ -5,10 +5,8 @@ import android.content.res.Configuration import android.os.Build import android.os.Handler import android.os.Looper -import android.util.AttributeSet import android.util.Log import android.view.View -import android.widget.FrameLayout import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReadableMap import com.facebook.react.uimanager.StateWrapper @@ -19,10 +17,8 @@ import com.swmansion.enriched.markdown.spoiler.SpoilerOverlay import com.swmansion.enriched.markdown.styles.StyleConfig import com.swmansion.enriched.markdown.utils.common.BreakStrategyUtils import com.swmansion.enriched.markdown.utils.common.CodeBlockStreamingMode -import com.swmansion.enriched.markdown.utils.common.FeatureFlags import com.swmansion.enriched.markdown.utils.common.MarkdownSegmentRenderer import com.swmansion.enriched.markdown.utils.common.RenderedSegment -import com.swmansion.enriched.markdown.utils.common.SegmentReconciler import com.swmansion.enriched.markdown.utils.common.StreamingMarkdownFilter import com.swmansion.enriched.markdown.utils.common.TableStreamingMode import com.swmansion.enriched.markdown.utils.common.isReducedMotionEnabled @@ -30,432 +26,431 @@ import com.swmansion.enriched.markdown.utils.common.splitASTIntoSegments import com.swmansion.enriched.markdown.utils.text.TailFadeInAnimator import com.swmansion.enriched.markdown.utils.text.view.SelectionMenuConfig import com.swmansion.enriched.markdown.utils.text.view.applySelectionColors -import com.swmansion.enriched.markdown.views.BlockSegmentView +import com.swmansion.enriched.markdown.views.BlockquoteContainerView import com.swmansion.enriched.markdown.views.CodeBlockContainerView +import com.swmansion.enriched.markdown.views.ContainerNodeView +import com.swmansion.enriched.markdown.views.SegmentViewConfig +import com.swmansion.enriched.markdown.views.SegmentViewCreators +import com.swmansion.enriched.markdown.views.SegmentViewFactory import com.swmansion.enriched.markdown.views.TableContainerView import java.util.EnumSet import java.util.concurrent.ExecutorService import java.util.concurrent.Executors -import kotlin.math.ceil -import kotlin.math.max - -class EnrichedMarkdown - @JvmOverloads - constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0, - ) : FrameLayout(context, attrs, defStyleAttr) { - private enum class DirtyFlag { - RECREATE_SEGMENTS, - FORCE_HEIGHT, - } - private val parser = Parser.shared - private val mainHandler = Handler(Looper.getMainLooper()) - private val executor: ExecutorService = Executors.newSingleThreadExecutor() - private val mathContainerClass: Class<*>? by lazy { - try { - Class.forName("com.swmansion.enriched.markdown.views.MathContainerView") - } catch (_: Exception) { - null - } - } +class EnrichedMarkdown( + context: Context, +) : ContainerNodeView(context) { + private enum class DirtyFlag { + RECREATE_SEGMENTS, + FORCE_HEIGHT, + } - private var currentRenderId = 0L - private val segmentViews = mutableListOf() - private val segmentSignatures = mutableListOf() - private val dirtyFlags = EnumSet.noneOf(DirtyFlag::class.java) - var streamingAnimation: Boolean = false - - // used to force a Yoga re-measure when a block image resolves its box height - var stateWrapper: StateWrapper? = null - private var forceHeightRecalculationCounter = 0 - - var tableStreamingMode: TableStreamingMode = TableStreamingMode.PROGRESSIVE - var codeBlockStreamingMode: CodeBlockStreamingMode = CodeBlockStreamingMode.PROGRESSIVE - private var renderPending: Boolean = false - - var currentMarkdown: String = "" - private set - - var markdownStyle: StyleConfig? = null - private set - - private var markdownStyleMap: ReadableMap? = null - private var lastKnownFontScale: Float = context.resources.configuration.fontScale - - var md4cFlags: Md4cFlags = Md4cFlags.DEFAULT - private set - private var allowFontScaling: Boolean = true - private var maxFontSizeMultiplier: Float = 0f - private var allowTrailingMargin: Boolean = false - private var imageRequestHeaders: Map = emptyMap() - private var selectable: Boolean = true - private var selectionColor: Int? = null - private var selectionHandleColor: Int? = null - private var selectionMenuConfig = SelectionMenuConfig() - private var accessibilityLabels = AccessibilityLabels() - private var textBreakStrategy: String = BreakStrategyUtils.DEFAULT_STRATEGY - - private var onLinkPressCallback: ((String) -> Unit)? = null - private var onLinkLongPressCallback: ((String) -> Unit)? = null - private var onTaskListItemPressCallback: ((Int, Boolean, String) -> Unit)? = null - private var onCopyPressCallback: ((String, String) -> Unit)? = null - private var contextMenuItemTexts: List = emptyList() - var onContextMenuItemPressCallback: ((itemText: String, selectedText: String, selectionStart: Int, selectionEnd: Int) -> Unit)? = null - var spoilerOverlay: SpoilerOverlay = SpoilerOverlay.PARTICLES - set(value) { - if (field == value) return - field = value - segmentViews.filterIsInstance().forEach { - it.spoilerOverlay = value - } + private val parser = Parser.shared + private val mainHandler = Handler(Looper.getMainLooper()) + private val executor: ExecutorService = Executors.newSingleThreadExecutor() + private val mathContainerClass: Class<*>? by lazy { SegmentViewCreators.mathContainerClass() } + + private var currentRenderId = 0L + private val dirtyFlags = EnumSet.noneOf(DirtyFlag::class.java) + var streamingAnimation: Boolean = false + + // used to force a Yoga re-measure when a block image resolves its box height + var stateWrapper: StateWrapper? = null + private var forceHeightRecalculationCounter = 0 + + var tableStreamingMode: TableStreamingMode = TableStreamingMode.PROGRESSIVE + var codeBlockStreamingMode: CodeBlockStreamingMode = CodeBlockStreamingMode.PROGRESSIVE + private var renderPending: Boolean = false + + var currentMarkdown: String = "" + private set + + var markdownStyle: StyleConfig? = null + private set + + private var markdownStyleMap: ReadableMap? = null + private var lastKnownFontScale: Float = context.resources.configuration.fontScale + + var md4cFlags: Md4cFlags = Md4cFlags.DEFAULT + private set + private var allowFontScaling: Boolean = true + private var maxFontSizeMultiplier: Float = 0f + private var imageRequestHeaders: Map = emptyMap() + private var selectable: Boolean = true + private var selectionColor: Int? = null + private var selectionHandleColor: Int? = null + private var selectionMenuConfig = SelectionMenuConfig() + private var accessibilityLabels = AccessibilityLabels() + private var textBreakStrategy: String = BreakStrategyUtils.DEFAULT_STRATEGY + + private var onLinkPressCallback: ((String) -> Unit)? = null + private var onLinkLongPressCallback: ((String) -> Unit)? = null + private var onTaskListItemPressCallback: ((Int, Boolean, String) -> Unit)? = null + private var onCopyPressCallback: ((String, String) -> Unit)? = null + private var contextMenuItemTexts: List = emptyList() + var onContextMenuItemPressCallback: ((itemText: String, selectedText: String, selectionStart: Int, selectionEnd: Int) -> Unit)? = null + var spoilerOverlay: SpoilerOverlay = SpoilerOverlay.PARTICLES + set(value) { + if (field == value) return + field = value + segmentViews.filterIsInstance().forEach { + it.spoilerOverlay = value } - var enableTaskListItemToggle: Boolean = true - set(value) { - if (field == value) return - field = value - segmentViews.filterIsInstance().forEach { - it.enableTaskListItemToggle = value - } + } + var enableTaskListItemToggle: Boolean = true + set(value) { + if (field == value) return + field = value + segmentViews.filterIsInstance().forEach { + it.enableTaskListItemToggle = value } - - fun setMarkdownContent(markdown: String) { - if (currentMarkdown == markdown) return - currentMarkdown = markdown - renderPending = true } - fun setMarkdownStyle(style: ReadableMap?) { - markdownStyleMap = style - val newConfig = style?.let { StyleConfig(it, context, allowFontScaling, maxFontSizeMultiplier) } - newConfig?.imageRequestHeaders = imageRequestHeaders - if (markdownStyle == newConfig) return - markdownStyle = newConfig - dirtyFlags += DirtyFlag.RECREATE_SEGMENTS - dirtyFlags += DirtyFlag.FORCE_HEIGHT - renderPending = true - } + init { + segmentViewFactory = RootFactory() + } - fun setImageRequestHeaders(headers: Map) { - if (imageRequestHeaders == headers) return - imageRequestHeaders = headers - markdownStyle?.imageRequestHeaders = headers - dirtyFlags += DirtyFlag.RECREATE_SEGMENTS - renderPending = true - } + fun setMarkdownContent(markdown: String) { + if (currentMarkdown == markdown) return + currentMarkdown = markdown + renderPending = true + } - fun commitProps() { - MeasurementStore.updateStreamingTableMode(id, tableStreamingMode) - MeasurementStore.updateStreamingCodeBlockMode(id, codeBlockStreamingMode) - MeasurementStore.updateFontScalingSettings(id, allowFontScaling, maxFontSizeMultiplier) - if (renderPending) { - renderPending = false - scheduleRenderIfNeeded() - } - } + fun setMarkdownStyle(style: ReadableMap?) { + markdownStyleMap = style + val newConfig = style?.let { StyleConfig(it, context, allowFontScaling, maxFontSizeMultiplier) } + newConfig?.imageRequestHeaders = imageRequestHeaders + if (markdownStyle == newConfig) return + markdownStyle = newConfig + dirtyFlags += DirtyFlag.RECREATE_SEGMENTS + dirtyFlags += DirtyFlag.FORCE_HEIGHT + renderPending = true + } - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - if (!allowFontScaling) return - val newFontScale = newConfig.fontScale - if (newFontScale != lastKnownFontScale) { - lastKnownFontScale = newFontScale - recreateStyleConfig() - dirtyFlags += DirtyFlag.RECREATE_SEGMENTS - dirtyFlags += DirtyFlag.FORCE_HEIGHT - scheduleRenderIfNeeded() - } - } + fun setImageRequestHeaders(headers: Map) { + if (imageRequestHeaders == headers) return + imageRequestHeaders = headers + markdownStyle?.imageRequestHeaders = headers + dirtyFlags += DirtyFlag.RECREATE_SEGMENTS + renderPending = true + } - fun setMd4cFlags(flags: Md4cFlags) { - if (md4cFlags == flags) return - md4cFlags = flags - renderPending = true + fun commitProps() { + MeasurementStore.updateStreamingTableMode(id, tableStreamingMode) + MeasurementStore.updateStreamingCodeBlockMode(id, codeBlockStreamingMode) + MeasurementStore.updateFontScalingSettings(id, allowFontScaling, maxFontSizeMultiplier) + if (renderPending) { + renderPending = false + scheduleRenderIfNeeded() } + } - fun setAllowFontScaling(allow: Boolean) { - if (allowFontScaling == allow) return - allowFontScaling = allow - MeasurementStore.updateFontScalingSettings(id, allowFontScaling, maxFontSizeMultiplier) + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + if (!allowFontScaling) return + val newFontScale = newConfig.fontScale + if (newFontScale != lastKnownFontScale) { + lastKnownFontScale = newFontScale recreateStyleConfig() dirtyFlags += DirtyFlag.RECREATE_SEGMENTS dirtyFlags += DirtyFlag.FORCE_HEIGHT - renderPending = true + scheduleRenderIfNeeded() } + } - fun setMaxFontSizeMultiplier(multiplier: Float) { - if (maxFontSizeMultiplier == multiplier) return - maxFontSizeMultiplier = multiplier - MeasurementStore.updateFontScalingSettings(id, allowFontScaling, maxFontSizeMultiplier) - recreateStyleConfig() - dirtyFlags += DirtyFlag.RECREATE_SEGMENTS - dirtyFlags += DirtyFlag.FORCE_HEIGHT - renderPending = true - } + fun setMd4cFlags(flags: Md4cFlags) { + if (md4cFlags == flags) return + md4cFlags = flags + renderPending = true + } - fun setAllowTrailingMargin(allow: Boolean) { - if (allowTrailingMargin == allow) return - allowTrailingMargin = allow - dirtyFlags += DirtyFlag.RECREATE_SEGMENTS - dirtyFlags += DirtyFlag.FORCE_HEIGHT - renderPending = true - } + fun setAllowFontScaling(allow: Boolean) { + if (allowFontScaling == allow) return + allowFontScaling = allow + MeasurementStore.updateFontScalingSettings(id, allowFontScaling, maxFontSizeMultiplier) + recreateStyleConfig() + dirtyFlags += DirtyFlag.RECREATE_SEGMENTS + dirtyFlags += DirtyFlag.FORCE_HEIGHT + renderPending = true + } - fun setIsSelectable(value: Boolean) { - if (selectable == value) return - selectable = value - segmentViews.filterIsInstance().forEach { - it.setIsSelectable(value) - } - } + fun setMaxFontSizeMultiplier(multiplier: Float) { + if (maxFontSizeMultiplier == multiplier) return + maxFontSizeMultiplier = multiplier + MeasurementStore.updateFontScalingSettings(id, allowFontScaling, maxFontSizeMultiplier) + recreateStyleConfig() + dirtyFlags += DirtyFlag.RECREATE_SEGMENTS + dirtyFlags += DirtyFlag.FORCE_HEIGHT + renderPending = true + } - fun setSelectionColor(color: Int?) { - if (selectionColor == color) return - selectionColor = color - applySelectionColorsToSegments() - } + fun setAllowTrailingMargin(allow: Boolean) { + if (trailingMarginEnabled == allow) return + trailingMarginEnabled = allow + dirtyFlags += DirtyFlag.RECREATE_SEGMENTS + dirtyFlags += DirtyFlag.FORCE_HEIGHT + renderPending = true + } - fun setSelectionHandleColor(color: Int?) { - if (selectionHandleColor == color) return - selectionHandleColor = color - applySelectionColorsToSegments() + fun setIsSelectable(value: Boolean) { + if (selectable == value) return + selectable = value + segmentViews.filterIsInstance().forEach { + it.setIsSelectable(value) } + } - fun setTextBreakStrategy(strategy: String) { - if (textBreakStrategy == strategy) return - textBreakStrategy = strategy - MeasurementStore.updateBreakStrategy(id, strategy) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val resolved = BreakStrategyUtils.resolveBreakStrategy(strategy) - segmentViews.filterIsInstance().forEach { - it.breakStrategy = resolved - } - } - dirtyFlags += DirtyFlag.FORCE_HEIGHT - renderPending = true - } + fun setSelectionColor(color: Int?) { + if (selectionColor == color) return + selectionColor = color + applySelectionColorsToSegments() + } + + fun setSelectionHandleColor(color: Int?) { + if (selectionHandleColor == color) return + selectionHandleColor = color + applySelectionColorsToSegments() + } - private fun applySelectionColorsToSegments() { + fun setTextBreakStrategy(strategy: String) { + if (textBreakStrategy == strategy) return + textBreakStrategy = strategy + MeasurementStore.updateBreakStrategy(id, strategy) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val resolved = BreakStrategyUtils.resolveBreakStrategy(strategy) segmentViews.filterIsInstance().forEach { - it.applySelectionColors(selectionColor, selectionHandleColor) + it.breakStrategy = resolved } } + dirtyFlags += DirtyFlag.FORCE_HEIGHT + renderPending = true + } - fun setOnLinkPressCallback(callback: (String) -> Unit) { - onLinkPressCallback = callback + private fun applySelectionColorsToSegments() { + segmentViews.filterIsInstance().forEach { + it.applySelectionColors(selectionColor, selectionHandleColor) } + } - fun setOnLinkLongPressCallback(callback: (String) -> Unit) { - onLinkLongPressCallback = callback - } + fun setOnLinkPressCallback(callback: (String) -> Unit) { + onLinkPressCallback = callback + } - fun setOnTaskListItemPressCallback(callback: ((taskIndex: Int, checked: Boolean, itemText: String) -> Unit)?) { - onTaskListItemPressCallback = callback - } + fun setOnLinkLongPressCallback(callback: (String) -> Unit) { + onLinkLongPressCallback = callback + } - fun setOnCopyPressCallback(callback: ((code: String, language: String) -> Unit)?) { - onCopyPressCallback = callback - } + fun setOnTaskListItemPressCallback(callback: ((taskIndex: Int, checked: Boolean, itemText: String) -> Unit)?) { + onTaskListItemPressCallback = callback + } - fun setContextMenuItems(items: List) { - contextMenuItemTexts = items - segmentViews.filterIsInstance().forEach { - it.setContextMenuItems(items, ::forwardContextMenuItemPress) - } + fun setOnCopyPressCallback(callback: ((code: String, language: String) -> Unit)?) { + onCopyPressCallback = callback + } + + fun setContextMenuItems(items: List) { + contextMenuItemTexts = items + segmentViews.filterIsInstance().forEach { + it.setContextMenuItems(items, ::forwardContextMenuItemPress) } + } - fun setSelectionMenuConfig(config: SelectionMenuConfig) { - if (selectionMenuConfig == config) return - selectionMenuConfig = config - segmentViews.filterIsInstance().forEach { - it.selectionMenuConfig = config - } - // Table and math views cache the copy labels, so re-push them on update - // (e.g. a language change without a remount) to avoid stale labels. - pushCopyLabelsToBlockSegments() + fun setSelectionMenuConfig(config: SelectionMenuConfig) { + if (selectionMenuConfig == config) return + selectionMenuConfig = config + segmentViews.filterIsInstance().forEach { + it.selectionMenuConfig = config } + // Table and math views cache the copy labels, so re-push them on update + // (e.g. a language change without a remount) to avoid stale labels. + pushCopyLabelsToBlockSegments() + } - private fun pushCopyLabelsToBlockSegments() { - val copyLabel = selectionMenuConfig.copyLabel - val copyAsMarkdownLabel = selectionMenuConfig.copyAsMarkdownLabel - segmentViews.forEach { view -> - when { - view is TableContainerView -> { - view.copyLabel = copyLabel - view.copyAsMarkdownLabel = copyAsMarkdownLabel - } + private fun pushCopyLabelsToBlockSegments() { + val copyLabel = selectionMenuConfig.copyLabel + val copyAsMarkdownLabel = selectionMenuConfig.copyAsMarkdownLabel + segmentViews.forEach { view -> + when { + view is TableContainerView -> { + view.copyLabel = copyLabel + view.copyAsMarkdownLabel = copyAsMarkdownLabel + } - view is CodeBlockContainerView -> { - view.copyLabel = copyLabel - view.copyAsMarkdownLabel = copyAsMarkdownLabel - } + view is CodeBlockContainerView -> { + view.copyLabel = copyLabel + view.copyAsMarkdownLabel = copyAsMarkdownLabel + } - isMathContainerView(view) -> { - runCatching { - view.javaClass.getMethod("setCopyLabel", String::class.java).invoke(view, copyLabel) - view.javaClass - .getMethod("setCopyAsMarkdownLabel", String::class.java) - .invoke(view, copyAsMarkdownLabel) - } + isMathContainerView(view) -> { + runCatching { + view.javaClass.getMethod("setCopyLabel", String::class.java).invoke(view, copyLabel) + view.javaClass + .getMethod("setCopyAsMarkdownLabel", String::class.java) + .invoke(view, copyAsMarkdownLabel) } } } } + } - fun setAccessibilityLabels(labels: AccessibilityLabels) { - if (accessibilityLabels == labels) return - accessibilityLabels = labels - segmentViews.filterIsInstance().forEach { - it.accessibilityLabels = labels - } - segmentViews.filterIsInstance().forEach { - it.accessibilityLabels = labels - } - mathContainerClass?.let { mathClass -> - segmentViews.filter { mathClass.isInstance(it) }.forEach { view -> - runCatching { - mathClass - .getMethod("setAccessibilityLabels", AccessibilityLabels::class.java) - .invoke(view, labels) - } + fun setAccessibilityLabels(labels: AccessibilityLabels) { + if (accessibilityLabels == labels) return + accessibilityLabels = labels + segmentViews.filterIsInstance().forEach { + it.accessibilityLabels = labels + } + segmentViews.filterIsInstance().forEach { + it.accessibilityLabels = labels + } + mathContainerClass?.let { mathClass -> + segmentViews.filter { mathClass.isInstance(it) }.forEach { view -> + runCatching { + mathClass + .getMethod("setAccessibilityLabels", AccessibilityLabels::class.java) + .invoke(view, labels) } } } + } - private fun forwardContextMenuItemPress( - itemText: String, - selectedText: String, - selectionStart: Int, - selectionEnd: Int, - ) { - onContextMenuItemPressCallback?.invoke(itemText, selectedText, selectionStart, selectionEnd) + private fun forwardContextMenuItemPress( + itemText: String, + selectedText: String, + selectionStart: Int, + selectionEnd: Int, + ) { + onContextMenuItemPressCallback?.invoke(itemText, selectedText, selectionStart, selectionEnd) + } + + private fun recreateStyleConfig() { + markdownStyleMap?.let { + markdownStyle = + StyleConfig(it, context, allowFontScaling, maxFontSizeMultiplier).also { config -> + config.imageRequestHeaders = imageRequestHeaders + } } + } + + private fun scheduleRenderIfNeeded() { + if (currentMarkdown.isNotEmpty()) scheduleRender() + } - private fun recreateStyleConfig() { - markdownStyleMap?.let { - markdownStyle = - StyleConfig(it, context, allowFontScaling, maxFontSizeMultiplier).also { config -> - config.imageRequestHeaders = imageRequestHeaders + private fun scheduleRender() { + val style = markdownStyle ?: return + val markdown = currentMarkdown.takeIf { it.isNotEmpty() } ?: return + val isStreaming = streamingAnimation + val tableMode = tableStreamingMode + val codeBlockMode = codeBlockStreamingMode + + val renderId = ++currentRenderId + + executor.execute { + try { + val filtered = + if (isStreaming) { + StreamingMarkdownFilter.renderableMarkdownForStreaming(markdown, tableMode, codeBlockMode) + } else { + null } - } - } + val renderableMarkdown = filtered?.markdown ?: markdown + val hasPendingCodeBlock = filtered?.endsInsideOpenCodeFence ?: false - private fun scheduleRenderIfNeeded() { - if (currentMarkdown.isNotEmpty()) scheduleRender() - } + if (renderableMarkdown.isEmpty()) { + postToMain(renderId) { applyRenderedSegments(emptyList(), style, false) } + return@execute + } - private fun scheduleRender() { - val style = markdownStyle ?: return - val markdown = currentMarkdown.takeIf { it.isNotEmpty() } ?: return - val isStreaming = streamingAnimation - val tableMode = tableStreamingMode - val codeBlockMode = codeBlockStreamingMode - - val renderId = ++currentRenderId - - executor.execute { - try { - val filtered = - if (isStreaming) { - StreamingMarkdownFilter.renderableMarkdownForStreaming(markdown, tableMode, codeBlockMode) - } else { - null - } - val renderableMarkdown = filtered?.markdown ?: markdown - val hasPendingCodeBlock = filtered?.endsInsideOpenCodeFence ?: false - - if (renderableMarkdown.isEmpty()) { + val ast = + parser.parseMarkdown(renderableMarkdown, md4cFlags) ?: run { postToMain(renderId) { applyRenderedSegments(emptyList(), style, false) } return@execute } - val ast = - parser.parseMarkdown(renderableMarkdown, md4cFlags) ?: run { - postToMain(renderId) { applyRenderedSegments(emptyList(), style, false) } - return@execute - } - - val segments = splitASTIntoSegments(ast) - val renderedSegments = - MarkdownSegmentRenderer.render( - segments, - style, - context, - onLinkPressCallback, - onLinkLongPressCallback, - ) - - postToMain(renderId) { applyRenderedSegments(renderedSegments, style, hasPendingCodeBlock) } - } catch (e: Exception) { - Log.e(TAG, "Render failed", e) - postToMain(renderId) { applyRenderedSegments(emptyList(), style, false) } - } + val segments = splitASTIntoSegments(ast) + val renderedSegments = + MarkdownSegmentRenderer.render( + segments, + style, + context, + onLinkPressCallback, + onLinkLongPressCallback, + ) + + postToMain(renderId) { applyRenderedSegments(renderedSegments, style, hasPendingCodeBlock) } + } catch (e: Exception) { + Log.e(TAG, "Render failed", e) + postToMain(renderId) { applyRenderedSegments(emptyList(), style, false) } } } + } - // Trailing code block whose closing fence hasn't streamed in yet, if any. - private var pendingCodeBlockSegment: RenderedSegment.CodeBlock? = null + // Trailing code block whose closing fence hasn't streamed in yet, if any. + private var pendingCodeBlockSegment: RenderedSegment.CodeBlock? = null - private fun applyRenderedSegments( - renderedSegments: List, - style: StyleConfig, - hasPendingCodeBlock: Boolean, - ) { - pendingCodeBlockSegment = - if (hasPendingCodeBlock) renderedSegments.lastOrNull() as? RenderedSegment.CodeBlock else null - - val reset = DirtyFlag.RECREATE_SEGMENTS in dirtyFlags - val forceHeight = DirtyFlag.FORCE_HEIGHT in dirtyFlags - dirtyFlags.clear() - - val result = - SegmentReconciler.reconcile( - currentViews = segmentViews.toList(), - currentSignatures = segmentSignatures.toList(), - renderedSegments = renderedSegments, - reset = reset, - matchesKind = ::viewMatchesSegmentKind, - createView = { segment -> - val view = createSegmentView(segment, style) - animateNewView(view, segment) - view - }, - updateView = { view, segment -> updateSegmentView(view, segment) }, - ) - - result.viewsToRemove.forEach { removeView(it) } - result.viewsToAttach.forEach { addView(it) } - - segmentViews.clear() - segmentViews.addAll(result.views) - segmentSignatures.clear() - segmentSignatures.addAll(result.signatures) - - // A just-closed block has unchanged content, so the reconciler reuses it - // without an update; sync pending here to trigger its deferred highlight. - segmentViews.forEachIndexed { index, view -> - if (view is CodeBlockContainerView) { - view.pending = pendingCodeBlockSegment != null && index == segmentViews.size - 1 - } - } + private fun applyRenderedSegments( + renderedSegments: List, + @Suppress("UNUSED_PARAMETER") style: StyleConfig, + hasPendingCodeBlock: Boolean, + ) { + pendingCodeBlockSegment = + if (hasPendingCodeBlock) renderedSegments.lastOrNull() as? RenderedSegment.CodeBlock else null - val topologyChanged = result.viewsToAttach.isNotEmpty() || result.viewsToRemove.isNotEmpty() + val reset = DirtyFlag.RECREATE_SEGMENTS in dirtyFlags + val forceHeight = DirtyFlag.FORCE_HEIGHT in dirtyFlags + dirtyFlags.clear() - if (width > 0) { - val heightBefore = computeSegmentsTotalHeight() - layoutSegments() - val heightAfter = computeSegmentsTotalHeight() + val topologyChanged = applySegments(renderedSegments, reset) - if (forceHeight || topologyChanged || heightBefore != heightAfter) { - MeasurementStore.invalidate(id) - requestLayout() - } + // A just-closed block has unchanged content, so the reconciler reuses it + // without an update; sync pending here to trigger its deferred highlight. + segmentViews.forEachIndexed { index, view -> + if (view is CodeBlockContainerView) { + view.pending = pendingCodeBlockSegment != null && index == segmentViews.size - 1 } } - private fun viewMatchesSegmentKind( + if (width > 0) { + val heightBefore = computeSegmentsTotalHeight() + layoutSegments() + val heightAfter = computeSegmentsTotalHeight() + + if (forceHeight || topologyChanged || heightBefore != heightAfter) { + MeasurementStore.invalidate(id) + requestLayout() + } + } + } + + private fun isMathContainerView(view: View): Boolean = mathContainerClass?.isInstance(view) == true + + private fun segmentViewConfig(): SegmentViewConfig = + SegmentViewConfig( + context = context, + style = markdownStyle!!, + allowFontScaling = allowFontScaling, + maxFontSizeMultiplier = maxFontSizeMultiplier, + accessibilityLabels = accessibilityLabels, + selectionMenuConfig = selectionMenuConfig, + textBreakStrategy = textBreakStrategy, + selectable = selectable, + selectionColor = selectionColor, + selectionHandleColor = selectionHandleColor, + contextMenuItemTexts = contextMenuItemTexts, + onLinkPress = onLinkPressCallback, + onLinkLongPress = onLinkLongPressCallback, + onCopyPress = onCopyPressCallback, + onTaskListItemPress = onTaskListItemPressCallback, + onContextMenuItemPress = ::forwardContextMenuItemPress, + ) + + /** + * Root factory: builds child views through the shared creators, then layers on + * root-only concerns (spoiler overlay mode, task-list toggle, streaming tail / + * fade-in animation, and the pending code block sync for the open trailing + * fence). + */ + private inner class RootFactory : SegmentViewFactory { + override fun matchesKind( view: View, segment: RenderedSegment, ): Boolean = @@ -464,22 +459,40 @@ class EnrichedMarkdown is RenderedSegment.Table -> view is TableContainerView is RenderedSegment.Math -> isMathContainerView(view) is RenderedSegment.CodeBlock -> view is CodeBlockContainerView + is RenderedSegment.Blockquote -> view is BlockquoteContainerView } - private fun isMathContainerView(view: View): Boolean = mathContainerClass?.isInstance(view) == true + override fun createView(segment: RenderedSegment): View { + val config = segmentViewConfig() + return when (segment) { + is RenderedSegment.Text -> { + SegmentViewCreators.createTextView(segment, config).apply { + spoilerOverlay = this@EnrichedMarkdown.spoilerOverlay + enableTaskListItemToggle = this@EnrichedMarkdown.enableTaskListItemToggle + } + } - private fun createSegmentView( - segment: RenderedSegment, - style: StyleConfig, - ): View = - when (segment) { - is RenderedSegment.Text -> createTextView(segment) - is RenderedSegment.Table -> createTableView(segment, style) - is RenderedSegment.Math -> createMathView(segment, style) - is RenderedSegment.CodeBlock -> createCodeBlockView(segment, style) + is RenderedSegment.Table -> { + SegmentViewCreators.createTableView(segment, config) + } + + is RenderedSegment.Math -> { + SegmentViewCreators.createMathView(segment, config) + } + + is RenderedSegment.CodeBlock -> { + SegmentViewCreators.createCodeBlockView(segment, config).apply { + pending = segment === pendingCodeBlockSegment + } + } + + is RenderedSegment.Blockquote -> { + SegmentViewCreators.createBlockquoteView(segment, config) + } } + } - private fun updateSegmentView( + override fun updateView( view: View, segment: RenderedSegment, ) { @@ -487,9 +500,7 @@ class EnrichedMarkdown is RenderedSegment.Text -> { val textView = view as EnrichedMarkdownInternalText val tailStart = textView.text?.length ?: 0 - textView.lastElementMarginBottom = segment.lastElementMarginBottom - textView.applyStyledText(segment.styledText) - segment.imageSpans.forEach { it.registerTextView(textView) } + SegmentViewCreators.updateTextView(textView, segment) animateTextViewTail(textView, tailStart) } @@ -503,9 +514,7 @@ class EnrichedMarkdown } is RenderedSegment.Math -> { - mathContainerClass - ?.getMethod("applyLatex", String::class.java) - ?.invoke(view, segment.latex) + SegmentViewCreators.updateMathView(view, segment) } is RenderedSegment.CodeBlock -> { @@ -514,226 +523,88 @@ class EnrichedMarkdown applyCodeBlockNode(segment.node) } } + + is RenderedSegment.Blockquote -> { + (view as BlockquoteContainerView).applyBlockquoteNode(segment.node) + } } } - private fun animateNewView( + override fun animateNewView( view: View, segment: RenderedSegment, ) { if (!streamingAnimation) return when (segment) { is RenderedSegment.Text -> animateTextViewTail(view as EnrichedMarkdownInternalText, 0) - is RenderedSegment.Table, is RenderedSegment.Math, is RenderedSegment.CodeBlock -> animateBlockViewFadeIn(view) - } - } - - private fun animateTextViewTail( - view: EnrichedMarkdownInternalText, - tailStart: Int, - ) { - if (!streamingAnimation) return - val textLength = view.text?.length ?: 0 - if (textLength <= tailStart) return - TailFadeInAnimator(view).animate(tailStart, textLength) - } - - private fun animateBlockViewFadeIn(view: View) { - if (!streamingAnimation) return - if (isReducedMotionEnabled(context)) return - view.alpha = 0f - view - .animate() - .alpha(1f) - .setDuration(BLOCK_FADE_DURATION_MS) - .start() - } - - private fun createTextView(segment: RenderedSegment.Text) = - EnrichedMarkdownInternalText(context).apply { - spoilerOverlay = this@EnrichedMarkdown.spoilerOverlay - enableTaskListItemToggle = this@EnrichedMarkdown.enableTaskListItemToggle - selectionMenuConfig = this@EnrichedMarkdown.selectionMenuConfig - accessibilityLabels = this@EnrichedMarkdown.accessibilityLabels - setIsSelectable(selectable) - markdownStyle?.paragraphStyle?.fontSize?.let { setTextSize(android.util.TypedValue.COMPLEX_UNIT_PX, it) } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - breakStrategy = BreakStrategyUtils.resolveBreakStrategy(textBreakStrategy) - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && segment.needsJustify) { - justificationMode = android.text.Layout.JUSTIFICATION_MODE_INTER_WORD - } - lastElementMarginBottom = segment.lastElementMarginBottom - applyStyledText(segment.styledText) - segment.imageSpans.forEach { it.registerTextView(this) } - - onTaskListItemPressCallback = { taskIndex, checked, itemText -> - this@EnrichedMarkdown.onTaskListItemPressCallback?.invoke(taskIndex, checked, itemText) - } - if (contextMenuItemTexts.isNotEmpty()) { - setContextMenuItems(contextMenuItemTexts, ::forwardContextMenuItemPress) - } - - applySelectionColors(selectionColor, selectionHandleColor) - } - - private fun createTableView( - segment: RenderedSegment.Table, - style: StyleConfig, - ) = TableContainerView(context, style).apply { - allowFontScaling = this@EnrichedMarkdown.allowFontScaling - maxFontSizeMultiplier = this@EnrichedMarkdown.maxFontSizeMultiplier - accessibilityLabels = this@EnrichedMarkdown.accessibilityLabels - onLinkPress = onLinkPressCallback - onLinkLongPress = onLinkLongPressCallback - copyLabel = this@EnrichedMarkdown.selectionMenuConfig.copyLabel - copyAsMarkdownLabel = this@EnrichedMarkdown.selectionMenuConfig.copyAsMarkdownLabel - applyTableNode(segment.node) - } - - private fun createCodeBlockView( - segment: RenderedSegment.CodeBlock, - style: StyleConfig, - ) = CodeBlockContainerView(context, style).apply { - copyLabel = this@EnrichedMarkdown.selectionMenuConfig.copyLabel - copyAsMarkdownLabel = this@EnrichedMarkdown.selectionMenuConfig.copyAsMarkdownLabel - onCopyPress = { code, language -> - this@EnrichedMarkdown.onCopyPressCallback?.invoke(code, language) - } - pending = segment === pendingCodeBlockSegment - applyCodeBlockNode(segment.node) - } - - private fun createMathView( - segment: RenderedSegment.Math, - style: StyleConfig, - ): View { - val resolvedClass = mathContainerClass - if (!FeatureFlags.IS_MATH_ENABLED || resolvedClass == null) return View(context) - return try { - val view = - resolvedClass - .getConstructor(Context::class.java, StyleConfig::class.java) - .newInstance(context, style) as View - runCatching { - resolvedClass - .getMethod("setAccessibilityLabels", AccessibilityLabels::class.java) - .invoke(view, accessibilityLabels) - } - resolvedClass - .getMethod("setCopyLabel", String::class.java) - .invoke(view, selectionMenuConfig.copyLabel) - resolvedClass - .getMethod("setCopyAsMarkdownLabel", String::class.java) - .invoke(view, selectionMenuConfig.copyAsMarkdownLabel) - resolvedClass.getMethod("applyLatex", String::class.java).invoke(view, segment.latex) - view - } catch (e: Exception) { - Log.e(TAG, "Failed to create math view", e) - View(context) + is RenderedSegment.Table, + is RenderedSegment.Math, + is RenderedSegment.CodeBlock, + is RenderedSegment.Blockquote, + -> animateBlockViewFadeIn(view) } } + } - private fun postToMain( - renderId: Long, - action: () -> Unit, - ) { - mainHandler.post { - if (renderId == currentRenderId) action() - } - } + private fun animateTextViewTail( + view: EnrichedMarkdownInternalText, + tailStart: Int, + ) { + if (!streamingAnimation) return + val textLength = view.text?.length ?: 0 + if (textLength <= tailStart) return + TailFadeInAnimator(view).animate(tailStart, textLength) + } - override fun onLayout( - changed: Boolean, - l: Int, - t: Int, - r: Int, - b: Int, - ) { - layoutSegments() - } + private fun animateBlockViewFadeIn(view: View) { + if (!streamingAnimation) return + if (isReducedMotionEnabled(context)) return + view.alpha = 0f + view + .animate() + .alpha(1f) + .setDuration(BLOCK_FADE_DURATION_MS) + .start() + } - fun onImageLayoutChanged() { - if (width > 0) { - layoutSegments() - requestLayout() - } - MeasurementStore.invalidate(id) - val wrapper = stateWrapper ?: return - val state = Arguments.createMap() - state.putInt("forceHeightRecalculationCounter", ++forceHeightRecalculationCounter) - wrapper.updateState(state) + private fun postToMain( + renderId: Long, + action: () -> Unit, + ) { + mainHandler.post { + if (renderId == currentRenderId) action() } + } - private fun layoutSegments() { - val containerWidth = width - if (containerWidth <= 0) return - - val needsOverhang = - segmentViews.any { view -> - view is TableContainerView && - ceil(view.tableStyle.horizontalOverflow.toDouble()).toInt() > 0 - } - if (clipChildren == needsOverhang) { - clipChildren = !needsOverhang - } - - var currentY = 0 - val lastIndex = segmentViews.lastIndex - val widthSpec = MeasureSpec.makeMeasureSpec(containerWidth, MeasureSpec.EXACTLY) - val heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) - - segmentViews.forEachIndexed { index, view -> - val segment = view as? BlockSegmentView - val shouldAddBottomMargin = index != lastIndex || allowTrailingMargin - - currentY += segment?.segmentMarginTop ?: 0 - - val overhang = - if (view is TableContainerView) { - max(ceil(view.tableStyle.horizontalOverflow.toDouble()).toInt(), 0) - } else { - 0 - } - - if (overhang > 0) { - val extendedWidth = containerWidth + overhang * 2 - val extWidthSpec = MeasureSpec.makeMeasureSpec(extendedWidth, MeasureSpec.EXACTLY) - view.measure(extWidthSpec, heightSpec) - view.layout(-overhang, currentY, containerWidth + overhang, currentY + view.measuredHeight) - } else { - view.measure(widthSpec, heightSpec) - view.layout(0, currentY, containerWidth, currentY + view.measuredHeight) - } - currentY += view.measuredHeight - - if (shouldAddBottomMargin) { - currentY += segment?.segmentMarginBottom ?: 0 - } - } - } + override fun onLayout( + changed: Boolean, + l: Int, + t: Int, + r: Int, + b: Int, + ) { + layoutSegments() + } - private fun computeSegmentsTotalHeight(): Int { - var totalHeight = 0 - val lastIndex = segmentViews.lastIndex - segmentViews.forEachIndexed { index, view -> - val segment = view as? BlockSegmentView - totalHeight += segment?.segmentMarginTop ?: 0 - totalHeight += view.measuredHeight - if (index != lastIndex || allowTrailingMargin) { - totalHeight += segment?.segmentMarginBottom ?: 0 - } - } - return totalHeight + fun onImageLayoutChanged() { + if (width > 0) { + layoutSegments() + requestLayout() } + MeasurementStore.invalidate(id) + val wrapper = stateWrapper ?: return + val state = Arguments.createMap() + state.putInt("forceHeightRecalculationCounter", ++forceHeightRecalculationCounter) + wrapper.updateState(state) + } - fun cleanup() { - executor.shutdownNow() - } + fun cleanup() { + executor.shutdownNow() + } - companion object { - private const val TAG = "EnrichedMarkdown" - private const val BLOCK_FADE_DURATION_MS = 200L - } + companion object { + private const val TAG = "EnrichedMarkdown" + private const val BLOCK_FADE_DURATION_MS = 200L } +} diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt index d6f8772e1..0ec4c9118 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt @@ -32,6 +32,7 @@ import com.swmansion.enriched.markdown.utils.common.getStringOrDefault import com.swmansion.enriched.markdown.utils.common.parseImageRequestHeaders import com.swmansion.enriched.markdown.utils.common.splitASTIntoSegments import com.swmansion.enriched.markdown.utils.text.extensions.replaceMathSpansWithPlaceholders +import com.swmansion.enriched.markdown.views.BlockquoteContainerView import com.swmansion.enriched.markdown.views.CodeBlockContainerView import com.swmansion.enriched.markdown.views.TableContainerView import java.util.concurrent.ConcurrentHashMap @@ -504,6 +505,15 @@ object MeasurementStore { totalHeightPx += style.codeBlockStyle.marginBottom } } + + is RenderedSegment.Blockquote -> { + totalHeightPx += style.blockquoteStyle.marginTop + totalHeightPx += BlockquoteContainerView.measureBlockquoteNodeHeight(segment.node, style, context, width) + maxContentWidthPx = width + if (includeBottomMargin) { + totalHeightPx += style.blockquoteStyle.marginBottom + } + } } } diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/Renderer.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/Renderer.kt index 459cc64ca..0aae10175 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/Renderer.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/Renderer.kt @@ -6,7 +6,9 @@ import android.text.SpannableStringBuilder import com.swmansion.enriched.markdown.parser.MarkdownASTNode import com.swmansion.enriched.markdown.spans.ImageSpan import com.swmansion.enriched.markdown.spans.MarginBottomSpan +import com.swmansion.enriched.markdown.styles.BlockquoteStyle import com.swmansion.enriched.markdown.styles.StyleConfig +import com.swmansion.enriched.markdown.utils.text.span.applyLineHeightSkippingImages class Renderer { private var cachedFactory: RendererFactory? = null @@ -59,6 +61,51 @@ class Renderer { return SpannableString(builder) } + /** + * Renders a blockquote's own content (the nodes left after nested quotes, code + * blocks and other block segments have been split out) styled as blockquote + * text: the blockquote block style is pushed so text picks up the quote's + * font/color and paragraphs render tight (isInsideBlockElement), matching the + * commonmark BlockquoteRenderer. It applies the quote's line height but draws no + * border/background/padding - the BlockquoteContainerView draws the box. + */ + fun renderBlockquoteContent( + nodes: List, + blockquoteStyle: BlockquoteStyle, + onLinkPress: ((String) -> Unit)? = null, + onLinkLongPress: ((String) -> Unit)? = null, + ): SpannableString { + val factory = + requireNotNull(cachedFactory) { + "Renderer must be configured with a style before calling renderBlockquoteContent." + } + + factory.resetForNewRender() + collectedImageSpans.clear() + lastElementMarginBottom = 0f + + val builder = SpannableStringBuilder() + val document = MarkdownASTNode(type = MarkdownASTNode.NodeType.Document, children = nodes) + val context = factory.blockStyleContext + + context.blockquoteDepth += 1 + context.setBlockquoteStyle(blockquoteStyle) + try { + renderNode(document, builder, onLinkPress, onLinkLongPress, factory) + } finally { + context.popBlockStyle() + context.blockquoteDepth -= 1 + } + + removeTrailingMargin(builder) + if (builder.isNotEmpty()) { + applyLineHeightSkippingImages(builder, 0, builder.length, blockquoteStyle.lineHeight) + } + factory.flushDeferredSpans(builder) + + return SpannableString(builder) + } + /** Removes trailing newlines and captures the margin of the final element. */ private fun removeTrailingMargin(builder: SpannableStringBuilder) { if (builder.isEmpty()) return diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownSegment.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownSegment.kt index 9d6af84a5..52a1b05d5 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownSegment.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownSegment.kt @@ -19,6 +19,10 @@ sealed interface MarkdownSegment { data class CodeBlock( val node: MarkdownASTNode, ) : MarkdownSegment + + data class Blockquote( + val node: MarkdownASTNode, + ) : MarkdownSegment } fun splitASTIntoSegments(root: MarkdownASTNode): List { @@ -55,6 +59,11 @@ fun splitASTIntoSegments(root: MarkdownASTNode): List { segments.add(MarkdownSegment.CodeBlock(child)) } + MarkdownASTNode.NodeType.Blockquote -> { + flushTextNodes() + segments.add(MarkdownSegment.Blockquote(child)) + } + else -> { currentTextNodes.add(child) } diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/RenderedSegment.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/RenderedSegment.kt index f5cfb7706..4880e7e24 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/RenderedSegment.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/RenderedSegment.kt @@ -5,6 +5,7 @@ import android.text.SpannableString import com.swmansion.enriched.markdown.parser.MarkdownASTNode import com.swmansion.enriched.markdown.renderer.Renderer import com.swmansion.enriched.markdown.spans.ImageSpan +import com.swmansion.enriched.markdown.styles.BlockquoteStyle import com.swmansion.enriched.markdown.styles.StyleConfig sealed interface RenderedSegment { @@ -32,6 +33,11 @@ sealed interface RenderedSegment { val node: MarkdownASTNode, override val signature: Long, ) : RenderedSegment + + data class Blockquote( + val node: MarkdownASTNode, + override val signature: Long, + ) : RenderedSegment } object MarkdownSegmentRenderer { @@ -41,11 +47,12 @@ object MarkdownSegmentRenderer { context: Context, onLinkPress: ((String) -> Unit)?, onLinkLongPress: ((String) -> Unit)?, + blockquoteStyle: BlockquoteStyle? = null, ): List = segments.map { segment -> when (segment) { is MarkdownSegment.Text -> { - renderTextSegment(segment.nodes, style, context, onLinkPress, onLinkLongPress) + renderTextSegment(segment.nodes, style, context, onLinkPress, onLinkLongPress, blockquoteStyle) } is MarkdownSegment.Table -> { @@ -63,6 +70,11 @@ object MarkdownSegmentRenderer { val signature = SegmentSignature.signatureForNode(segment.node) xor SegmentSignature.CODE_BLOCK_KIND_SALT RenderedSegment.CodeBlock(segment.node, signature) } + + is MarkdownSegment.Blockquote -> { + val signature = SegmentSignature.signatureForNode(segment.node) xor SegmentSignature.BLOCKQUOTE_KIND_SALT + RenderedSegment.Blockquote(segment.node, signature) + } } } @@ -72,13 +84,21 @@ object MarkdownSegmentRenderer { context: Context, onLinkPress: ((String) -> Unit)?, onLinkLongPress: ((String) -> Unit)?, + blockquoteStyle: BlockquoteStyle?, ): RenderedSegment.Text { - val documentWrapper = MarkdownASTNode(type = MarkdownASTNode.NodeType.Document, children = nodes) val renderer = Renderer().apply { configure(style, context) } val signature = SegmentSignature.signatureForNodes(nodes) xor SegmentSignature.TEXT_KIND_SALT + val styledText = + if (blockquoteStyle != null) { + renderer.renderBlockquoteContent(nodes, blockquoteStyle, onLinkPress, onLinkLongPress) + } else { + val documentWrapper = MarkdownASTNode(type = MarkdownASTNode.NodeType.Document, children = nodes) + renderer.renderDocument(documentWrapper, onLinkPress, onLinkLongPress) + } + return RenderedSegment.Text( - styledText = renderer.renderDocument(documentWrapper, onLinkPress, onLinkLongPress), + styledText = styledText, imageSpans = renderer.getCollectedImageSpans().toList(), needsJustify = style.needsJustify, lastElementMarginBottom = renderer.getLastElementMarginBottom(), diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/SegmentSignature.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/SegmentSignature.kt index 5d31ef7d8..76d945e3b 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/SegmentSignature.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/SegmentSignature.kt @@ -12,6 +12,7 @@ object SegmentSignature { internal const val TABLE_KIND_SALT = 0x7461626C00000000L // "tabl" internal const val MATH_KIND_SALT = 0x6D61746800000000L // "math" internal const val CODE_BLOCK_KIND_SALT = 0x63626C6B00000000L // "cblk" + internal const val BLOCKQUOTE_KIND_SALT = 0x6271746500000000L // "bqte" private fun fnvMixByte( hash: Long, diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/BlockquoteContainerView.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/BlockquoteContainerView.kt new file mode 100644 index 000000000..10290037c --- /dev/null +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/BlockquoteContainerView.kt @@ -0,0 +1,253 @@ +package com.swmansion.enriched.markdown.views + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Path +import android.graphics.RectF +import android.view.View +import androidx.core.graphics.withSave +import com.swmansion.enriched.markdown.EnrichedMarkdownInternalText +import com.swmansion.enriched.markdown.parser.MarkdownASTNode +import com.swmansion.enriched.markdown.styles.BlockquoteStyle +import com.swmansion.enriched.markdown.styles.StyleConfig +import com.swmansion.enriched.markdown.utils.common.BreakStrategyUtils +import com.swmansion.enriched.markdown.utils.common.MarkdownSegmentRenderer +import com.swmansion.enriched.markdown.utils.common.RenderedSegment +import com.swmansion.enriched.markdown.utils.common.splitASTIntoSegments +import kotlin.math.ceil + +/** + * A GFM blockquote rendered as a recursive container: it splits its own AST + * children into segments and lays them out through the shared ContainerNodeView + * machinery, so a nested quote becomes another BlockquoteContainerView, a fenced + * code block becomes a CodeBlockContainerView, and prose collapses into a single + * text view. Nesting depth is automatic: each level is its own view, inset + * further by its padding, so onDraw only ever renders one accent bar plus the + * background for this single level. + * + * The content inset is applied as this view's padding (left = borderWidth + + * gapWidth + padding, other edges = padding), mirroring the geometry + * BlockquoteSpan draws for the in-text (list-nested) path; the base measure and + * layout already honor padding. + * + * Streaming is treated as static inside a quote (no pending-fence handling, no + * tail/fade animation) to keep the recursion tractable; a content change re-signs + * the whole container so the reconciler still reuses the outer quote view. + */ +class BlockquoteContainerView( + context: Context, + private val parentConfig: SegmentViewConfig, +) : ContainerNodeView(context), + BlockSegmentView { + private val blockquoteStyle: BlockquoteStyle = parentConfig.style.blockquoteStyle + + private val borderWidthPx: Float = blockquoteStyle.borderWidth + private val paddingPx: Float = blockquoteStyle.padding + private val leftInset: Int = ceil(borderWidthPx + blockquoteStyle.gapWidth + paddingPx).toInt() + private val verticalInset: Int = ceil(paddingPx).toInt() + private val rightInset: Int = ceil(paddingPx).toInt() + + // Only the outermost quote carries vertical margins; a quote nested directly + // inside another quote is separated by the parent's padding alone (matches the + // commonmark BlockquoteRenderer, which applies margins only at depth 0). + var nested: Boolean = false + + override val segmentMarginTop: Int get() = if (nested) 0 else blockquoteStyle.marginTop.toInt() + override val segmentMarginBottom: Int get() = if (nested) 0 else blockquoteStyle.marginBottom.toInt() + + private val backgroundPaint = + Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL } + private val borderPaint = + Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.FILL + color = blockquoteStyle.borderColor + } + private val rect = RectF() + private val path = Path() + private val radiiArray = FloatArray(8) + + init { + setWillNotDraw(false) + setPadding(leftInset, verticalInset, rightInset, verticalInset) + segmentViewFactory = ChildFactory() + trailingMarginEnabled = false + } + + fun applyBlockquoteNode(node: MarkdownASTNode) { + val segments = splitASTIntoSegments(node) + val rendered = + MarkdownSegmentRenderer.render( + segments, + parentConfig.style, + context, + parentConfig.onLinkPress, + parentConfig.onLinkLongPress, + blockquoteStyle, + ) + applySegments(rendered, reset = false) + requestLayout() + } + + override fun onMeasure( + widthSpec: Int, + heightSpec: Int, + ) { + val measuredWidth = MeasureSpec.getSize(widthSpec) + measureSegmentChildren(measuredWidth) + setMeasuredDimension(measuredWidth, computeSegmentsTotalHeight()) + } + + override fun onLayout( + changed: Boolean, + l: Int, + t: Int, + r: Int, + b: Int, + ) { + layoutSegments() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + val radius = blockquoteStyle.borderRadius + val bgColor = blockquoteStyle.backgroundColor?.takeIf { it != Color.TRANSPARENT } + val hasRadius = radius > 0f + + if (hasRadius) { + rect.set(0f, 0f, width.toFloat(), height.toFloat()) + radiiArray.fill(radius) + path.reset() + path.addRoundRect(rect, radiiArray, Path.Direction.CW) + } + + if (bgColor != null) { + backgroundPaint.color = bgColor + if (hasRadius) { + canvas.drawPath(path, backgroundPaint) + } else { + canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), backgroundPaint) + } + } + + // Clip the accent bar to the rounded box so its top-left/bottom-left corners + // follow the border radius instead of poking past the background as squares. + if (hasRadius) { + canvas.withSave { + clipPath(path) + drawRect(0f, 0f, borderWidthPx, height.toFloat(), borderPaint) + } + } else { + canvas.drawRect(0f, 0f, borderWidthPx, height.toFloat(), borderPaint) + } + } + + /** + * Factory for this quote's own children. It reuses the shared creators for + * every kind and, for a nested Blockquote, creates another + * BlockquoteContainerView (the recursion). Streaming is static here: no + * animation, no pending-fence sync. + */ + private inner class ChildFactory : SegmentViewFactory { + override fun matchesKind( + view: View, + segment: RenderedSegment, + ): Boolean = + when (segment) { + is RenderedSegment.Text -> view is EnrichedMarkdownInternalText + is RenderedSegment.Table -> view is TableContainerView + is RenderedSegment.Math -> SegmentViewCreators.isMathContainerView(view) + is RenderedSegment.CodeBlock -> view is CodeBlockContainerView + is RenderedSegment.Blockquote -> view is BlockquoteContainerView + } + + override fun createView(segment: RenderedSegment): View = + when (segment) { + is RenderedSegment.Text -> { + SegmentViewCreators.createTextView(segment, parentConfig) + } + + is RenderedSegment.Table -> { + SegmentViewCreators.createTableView(segment, parentConfig) + } + + is RenderedSegment.Math -> { + SegmentViewCreators.createMathView(segment, parentConfig) + } + + is RenderedSegment.CodeBlock -> { + SegmentViewCreators.createCodeBlockView(segment, parentConfig) + } + + is RenderedSegment.Blockquote -> { + SegmentViewCreators.createBlockquoteView(segment, parentConfig).apply { nested = true } + } + } + + override fun updateView( + view: View, + segment: RenderedSegment, + ) { + when (segment) { + is RenderedSegment.Text -> SegmentViewCreators.updateTextView(view as EnrichedMarkdownInternalText, segment) + is RenderedSegment.Table -> (view as TableContainerView).applyTableNode(segment.node) + is RenderedSegment.Math -> SegmentViewCreators.updateMathView(view, segment) + is RenderedSegment.CodeBlock -> (view as CodeBlockContainerView).applyCodeBlockNode(segment.node) + is RenderedSegment.Blockquote -> (view as BlockquoteContainerView).applyBlockquoteNode(segment.node) + } + } + + override fun animateNewView( + view: View, + segment: RenderedSegment, + ) = Unit + } + + companion object { + /** + * View-free height of a blockquote node at the given outer content width. + * The children are summed at the reduced inner width (outer minus horizontal + * inset) and the vertical inset is added; nested quotes recurse through this + * same function via SegmentHeightMeasurer. Math child heights use the shared + * estimate to avoid a main-thread measure from the layout pass. + */ + fun measureBlockquoteNodeHeight( + node: MarkdownASTNode, + config: StyleConfig, + context: Context, + width: Float, + ): Float { + val style = config.blockquoteStyle + val leftInset = ceil(style.borderWidth + style.gapWidth + style.padding).toInt() + val rightInset = ceil(style.padding).toInt() + val verticalInset = ceil(style.padding).toInt() + val innerWidth = (width - leftInset - rightInset).coerceAtLeast(1f) + + val segments = splitASTIntoSegments(node) + val rendered = MarkdownSegmentRenderer.render(segments, config, context, null, null, style) + + val fontSize = style.fontSize + val breakStrategy = BreakStrategyUtils.resolveBreakStrategy(null) + + val childrenHeight = + SegmentHeightMeasurer.measureSegmentsHeight( + segments = rendered, + style = config, + context = context, + contentWidthPx = innerWidth, + fontSizePx = fontSize, + breakStrategy = breakStrategy, + allowTrailingMargin = false, + mathHeightForIndex = { estimateMathHeight(config) }, + ) + + return childrenHeight + verticalInset * 2f + } + + private fun estimateMathHeight(config: StyleConfig): Float { + val estimatedHeight = config.mathStyle.fontSize * 1.4f + return estimatedHeight + config.mathStyle.padding * 2 + } + } +} diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/ContainerNodeView.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/ContainerNodeView.kt new file mode 100644 index 000000000..85370f745 --- /dev/null +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/ContainerNodeView.kt @@ -0,0 +1,155 @@ +package com.swmansion.enriched.markdown.views + +import android.content.Context +import android.view.View +import android.widget.FrameLayout +import com.swmansion.enriched.markdown.utils.common.RenderedSegment +import com.swmansion.enriched.markdown.utils.common.SegmentReconciler +import kotlin.math.ceil +import kotlin.math.max + +/** + * Reusable view for an AST branch node that holds a vertical stack of block + * children (the document root and every blockquote). It owns the machinery + * shared by both: reconciling a list of RenderedSegment into real child views + * by signature, stacking them vertically with per-segment margins, and + * reporting the total height (including its own vertical padding). + * + * Per-kind view construction is delegated to a SegmentViewFactory the subclass + * supplies, so root-only wiring (link/copy callbacks, selection menu, spoiler, + * accessibility, task-list toggle, pending code block) lives with the root and + * the nested blockquote factory reuses only the shared creators. + * + * Layout honors paddingLeft/Top/Right/Bottom so a subclass can inset its + * children (e.g. a blockquote's border/gap/padding) via padding: children are + * measured at exactly width - paddingLeft - paddingRight and laid out starting + * at (paddingLeft, paddingTop). The root uses zero padding, so its behavior is + * unchanged. + */ +open class ContainerNodeView( + context: Context, +) : FrameLayout(context) { + protected val segmentViews = mutableListOf() + protected val segmentSignatures = mutableListOf() + + protected lateinit var segmentViewFactory: SegmentViewFactory + + protected var trailingMarginEnabled: Boolean = false + + /** + * Reconciles the current child views against renderedSegments, attaching and + * removing views as needed and updating segmentViews/segmentSignatures. + * Returns whether the topology changed (any view attached or removed). + */ + protected fun applySegments( + renderedSegments: List, + reset: Boolean, + ): Boolean { + val result = + SegmentReconciler.reconcile( + currentViews = segmentViews.toList(), + currentSignatures = segmentSignatures.toList(), + renderedSegments = renderedSegments, + reset = reset, + matchesKind = segmentViewFactory::matchesKind, + createView = { segment -> + val view = segmentViewFactory.createView(segment) + segmentViewFactory.animateNewView(view, segment) + view + }, + updateView = { view, segment -> segmentViewFactory.updateView(view, segment) }, + ) + + result.viewsToRemove.forEach { removeView(it) } + result.viewsToAttach.forEach { addView(it) } + + segmentViews.clear() + segmentViews.addAll(result.views) + segmentSignatures.clear() + segmentSignatures.addAll(result.signatures) + + return result.viewsToAttach.isNotEmpty() || result.viewsToRemove.isNotEmpty() + } + + protected fun layoutSegments() { + val containerWidth = width + if (containerWidth <= 0) return + + val contentWidth = (containerWidth - paddingLeft - paddingRight).coerceAtLeast(0) + + val needsOverhang = + segmentViews.any { view -> + view is TableContainerView && + ceil(view.tableStyle.horizontalOverflow.toDouble()).toInt() > 0 + } + if (clipChildren == needsOverhang) { + clipChildren = !needsOverhang + } + + var currentY = paddingTop + val lastIndex = segmentViews.lastIndex + val widthSpec = MeasureSpec.makeMeasureSpec(contentWidth, MeasureSpec.EXACTLY) + val heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) + + segmentViews.forEachIndexed { index, view -> + val segment = view as? BlockSegmentView + val shouldAddBottomMargin = index != lastIndex || trailingMarginEnabled + + currentY += segment?.segmentMarginTop ?: 0 + + val overhang = + if (view is TableContainerView) { + max(ceil(view.tableStyle.horizontalOverflow.toDouble()).toInt(), 0) + } else { + 0 + } + + if (overhang > 0) { + val extendedWidth = contentWidth + overhang * 2 + val extWidthSpec = MeasureSpec.makeMeasureSpec(extendedWidth, MeasureSpec.EXACTLY) + view.measure(extWidthSpec, heightSpec) + view.layout( + paddingLeft - overhang, + currentY, + paddingLeft + contentWidth + overhang, + currentY + view.measuredHeight, + ) + } else { + view.measure(widthSpec, heightSpec) + view.layout(paddingLeft, currentY, paddingLeft + contentWidth, currentY + view.measuredHeight) + } + currentY += view.measuredHeight + + if (shouldAddBottomMargin) { + currentY += segment?.segmentMarginBottom ?: 0 + } + } + } + + /** + * Measures every child at the inner content width so computeSegmentsTotalHeight + * can read accurate measuredHeights during a subclass's onMeasure, before the + * onLayout pass runs layoutSegments. Without this a container that sizes itself + * from its children (a blockquote) would measure them as zero-height and clip. + */ + protected fun measureSegmentChildren(outerWidth: Int) { + val contentWidth = (outerWidth - paddingLeft - paddingRight).coerceAtLeast(0) + val widthSpec = MeasureSpec.makeMeasureSpec(contentWidth, MeasureSpec.EXACTLY) + val heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) + segmentViews.forEach { it.measure(widthSpec, heightSpec) } + } + + protected fun computeSegmentsTotalHeight(): Int { + var totalHeight = paddingTop + paddingBottom + val lastIndex = segmentViews.lastIndex + segmentViews.forEachIndexed { index, view -> + val segment = view as? BlockSegmentView + totalHeight += segment?.segmentMarginTop ?: 0 + totalHeight += view.measuredHeight + if (index != lastIndex || trailingMarginEnabled) { + totalHeight += segment?.segmentMarginBottom ?: 0 + } + } + return totalHeight + } +} diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/SegmentHeightMeasurer.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/SegmentHeightMeasurer.kt new file mode 100644 index 000000000..7ea237463 --- /dev/null +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/SegmentHeightMeasurer.kt @@ -0,0 +1,125 @@ +package com.swmansion.enriched.markdown.views + +import android.content.Context +import android.graphics.Paint +import android.os.Build +import android.text.Layout +import android.text.StaticLayout +import android.text.TextPaint +import com.swmansion.enriched.markdown.styles.StyleConfig +import com.swmansion.enriched.markdown.utils.common.RenderedSegment +import com.swmansion.enriched.markdown.utils.text.extensions.replaceMathSpansWithPlaceholders +import kotlin.math.ceil + +/** + * View-free height summation for a vertical stack of RenderedSegments at a fixed + * content width, single-sourced so the shadow-node measurement pass + * (MeasurementStore) and the blockquote container's own static measurer report + * identical heights. + * + * Math heights must be measured up front on the main thread by the caller and + * supplied through mathHeightForIndex, because the summation runs off the main + * thread. Blockquote children recurse through + * BlockquoteContainerView.measureBlockquoteNodeHeight. + */ +object SegmentHeightMeasurer { + fun measureSegmentsHeight( + segments: List, + style: StyleConfig, + context: Context, + contentWidthPx: Float, + fontSizePx: Float, + breakStrategy: Int, + allowTrailingMargin: Boolean, + mathHeightForIndex: (Int) -> Float, + ): Float { + val widthPx = ceil(contentWidthPx).toInt().coerceAtLeast(1) + val lastIndex = segments.lastIndex + var totalHeightPx = 0f + + for ((index, segment) in segments.withIndex()) { + val isLastSegment = index == lastIndex + val includeBottomMargin = if (isLastSegment) allowTrailingMargin else true + + when (segment) { + is RenderedSegment.Text -> { + segment.styledText.replaceMathSpansWithPlaceholders(context) + val layout = createStaticLayout(segment.styledText, fontSizePx, widthPx, breakStrategy) + totalHeightPx += layout.height + if (includeBottomMargin) { + totalHeightPx += segment.lastElementMarginBottom + } + } + + is RenderedSegment.Table -> { + totalHeightPx += style.tableStyle.marginTop + totalHeightPx += TableContainerView.measureTableNodeHeight(segment.node, style, context) + if (includeBottomMargin) { + totalHeightPx += style.tableStyle.marginBottom + } + } + + is RenderedSegment.Math -> { + totalHeightPx += style.mathStyle.marginTop + totalHeightPx += mathHeightForIndex(index) + if (includeBottomMargin) { + totalHeightPx += style.mathStyle.marginBottom + } + } + + is RenderedSegment.CodeBlock -> { + totalHeightPx += style.codeBlockStyle.marginTop + totalHeightPx += CodeBlockContainerView.measureCodeBlockNodeHeight(segment.node, style, context, contentWidthPx) + if (includeBottomMargin) { + totalHeightPx += style.codeBlockStyle.marginBottom + } + } + + is RenderedSegment.Blockquote -> { + // A quote encountered here is always nested inside another quote, so it + // carries no vertical margin (only the outermost quote does, added by + // MeasurementStore); this mirrors BlockquoteContainerView.nested. + totalHeightPx += BlockquoteContainerView.measureBlockquoteNodeHeight(segment.node, style, context, contentWidthPx) + } + } + } + return totalHeightPx + } + + fun createStaticLayout( + text: CharSequence, + fontSizePx: Float, + widthPx: Int, + breakStrategy: Int, + ): StaticLayout { + val paint = + TextPaint(Paint.ANTI_ALIAS_FLAG).apply { + textSize = fontSizePx + } + prepareImageSpansForMeasurement(text, widthPx) + return StaticLayout.Builder + .obtain(text, 0, text.length, paint, widthPx) + .setIncludePad(false) + .setLineSpacing(0f, 1f) + .apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + @Suppress("WrongConstant") + setBreakStrategy(breakStrategy) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + setUseLineSpacingFromFallbacks(true) + } + }.build() + } + + private fun prepareImageSpansForMeasurement( + text: CharSequence?, + widthPx: Int, + ) { + if (widthPx <= 1) return + val spanned = text as? android.text.Spanned ?: return + spanned + .getSpans(0, spanned.length, com.swmansion.enriched.markdown.spans.ImageSpan::class.java) + .forEach { it.prepareForMeasurement(spanned, widthPx) } + } +} diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/SegmentViewCreators.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/SegmentViewCreators.kt new file mode 100644 index 000000000..0d0d07102 --- /dev/null +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/SegmentViewCreators.kt @@ -0,0 +1,177 @@ +package com.swmansion.enriched.markdown.views + +import android.content.Context +import android.os.Build +import android.text.Layout +import android.util.Log +import android.util.TypedValue +import android.view.View +import com.swmansion.enriched.markdown.EnrichedMarkdownInternalText +import com.swmansion.enriched.markdown.accessibility.AccessibilityLabels +import com.swmansion.enriched.markdown.styles.StyleConfig +import com.swmansion.enriched.markdown.utils.common.BreakStrategyUtils +import com.swmansion.enriched.markdown.utils.common.FeatureFlags +import com.swmansion.enriched.markdown.utils.common.RenderedSegment +import com.swmansion.enriched.markdown.utils.text.view.SelectionMenuConfig +import com.swmansion.enriched.markdown.utils.text.view.applySelectionColors + +/** + * Configuration shared by every ContainerNodeView's SegmentViewFactory so that + * Text / Table / Math / CodeBlock / Blockquote child views are constructed the + * same way regardless of the host (root document or nested blockquote). + * + * The root supplies the full wiring; a nested blockquote supplies the subset it + * needs (styling, link/copy callbacks, accessibility labels) and treats + * streaming as static. + */ +data class SegmentViewConfig( + val context: Context, + val style: StyleConfig, + val allowFontScaling: Boolean, + val maxFontSizeMultiplier: Float, + val accessibilityLabels: AccessibilityLabels, + val selectionMenuConfig: SelectionMenuConfig, + val textBreakStrategy: String, + val selectable: Boolean, + val selectionColor: Int?, + val selectionHandleColor: Int?, + val contextMenuItemTexts: List, + val onLinkPress: ((String) -> Unit)?, + val onLinkLongPress: ((String) -> Unit)?, + val onCopyPress: ((code: String, language: String) -> Unit)?, + val onTaskListItemPress: ((taskIndex: Int, checked: Boolean, itemText: String) -> Unit)?, + val onContextMenuItemPress: ((itemText: String, selectedText: String, selectionStart: Int, selectionEnd: Int) -> Unit)?, +) + +/** + * Pure per-kind child view creators used by both the root and nested factories. + * Kept free of host-only concerns (streaming tail animation, pending fence + * handling, spoiler overlay mode, task-list toggle) so the root can add those on + * top while nested blockquotes reuse the exact same construction. + */ +object SegmentViewCreators { + private const val TAG = "SegmentViewCreators" + + fun createTextView( + segment: RenderedSegment.Text, + config: SegmentViewConfig, + ): EnrichedMarkdownInternalText = + EnrichedMarkdownInternalText(config.context).apply { + selectionMenuConfig = config.selectionMenuConfig + accessibilityLabels = config.accessibilityLabels + setIsSelectable(config.selectable) + setTextSize(TypedValue.COMPLEX_UNIT_PX, config.style.paragraphStyle.fontSize) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + breakStrategy = BreakStrategyUtils.resolveBreakStrategy(config.textBreakStrategy) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && segment.needsJustify) { + justificationMode = Layout.JUSTIFICATION_MODE_INTER_WORD + } + lastElementMarginBottom = segment.lastElementMarginBottom + applyStyledText(segment.styledText) + segment.imageSpans.forEach { it.registerTextView(this) } + + onTaskListItemPressCallback = { taskIndex, checked, itemText -> + config.onTaskListItemPress?.invoke(taskIndex, checked, itemText) + } + + if (config.contextMenuItemTexts.isNotEmpty()) { + val onPress = config.onContextMenuItemPress + if (onPress != null) { + setContextMenuItems(config.contextMenuItemTexts) { itemText, selectedText, start, end -> + onPress(itemText, selectedText, start, end) + } + } + } + + applySelectionColors(config.selectionColor, config.selectionHandleColor) + } + + fun updateTextView( + view: EnrichedMarkdownInternalText, + segment: RenderedSegment.Text, + ) { + view.lastElementMarginBottom = segment.lastElementMarginBottom + view.applyStyledText(segment.styledText) + segment.imageSpans.forEach { it.registerTextView(view) } + } + + fun createTableView( + segment: RenderedSegment.Table, + config: SegmentViewConfig, + ) = TableContainerView(config.context, config.style).apply { + allowFontScaling = config.allowFontScaling + maxFontSizeMultiplier = config.maxFontSizeMultiplier + accessibilityLabels = config.accessibilityLabels + onLinkPress = config.onLinkPress + onLinkLongPress = config.onLinkLongPress + copyLabel = config.selectionMenuConfig.copyLabel + copyAsMarkdownLabel = config.selectionMenuConfig.copyAsMarkdownLabel + applyTableNode(segment.node) + } + + fun createCodeBlockView( + segment: RenderedSegment.CodeBlock, + config: SegmentViewConfig, + ) = CodeBlockContainerView(config.context, config.style).apply { + copyLabel = config.selectionMenuConfig.copyLabel + copyAsMarkdownLabel = config.selectionMenuConfig.copyAsMarkdownLabel + onCopyPress = { code, language -> config.onCopyPress?.invoke(code, language) } + applyCodeBlockNode(segment.node) + } + + fun mathContainerClass(): Class<*>? = + try { + Class.forName("com.swmansion.enriched.markdown.views.MathContainerView") + } catch (_: Exception) { + null + } + + fun isMathContainerView(view: View): Boolean = mathContainerClass()?.isInstance(view) == true + + fun createMathView( + segment: RenderedSegment.Math, + config: SegmentViewConfig, + ): View { + val resolvedClass = mathContainerClass() + if (!FeatureFlags.IS_MATH_ENABLED || resolvedClass == null) return View(config.context) + return try { + val view = + resolvedClass + .getConstructor(Context::class.java, StyleConfig::class.java) + .newInstance(config.context, config.style) as View + runCatching { + resolvedClass + .getMethod("setAccessibilityLabels", AccessibilityLabels::class.java) + .invoke(view, config.accessibilityLabels) + } + resolvedClass + .getMethod("setCopyLabel", String::class.java) + .invoke(view, config.selectionMenuConfig.copyLabel) + resolvedClass + .getMethod("setCopyAsMarkdownLabel", String::class.java) + .invoke(view, config.selectionMenuConfig.copyAsMarkdownLabel) + resolvedClass.getMethod("applyLatex", String::class.java).invoke(view, segment.latex) + view + } catch (e: Exception) { + Log.e(TAG, "Failed to create math view", e) + View(config.context) + } + } + + fun updateMathView( + view: View, + segment: RenderedSegment.Math, + ) { + mathContainerClass() + ?.getMethod("applyLatex", String::class.java) + ?.invoke(view, segment.latex) + } + + fun createBlockquoteView( + segment: RenderedSegment.Blockquote, + config: SegmentViewConfig, + ) = BlockquoteContainerView(config.context, config).apply { + applyBlockquoteNode(segment.node) + } +} diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/SegmentViewFactory.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/SegmentViewFactory.kt new file mode 100644 index 000000000..75f20f75c --- /dev/null +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/SegmentViewFactory.kt @@ -0,0 +1,30 @@ +package com.swmansion.enriched.markdown.views + +import android.view.View +import com.swmansion.enriched.markdown.utils.common.RenderedSegment + +/** + * Per-kind view work seam used by ContainerNodeView to build and reconcile + * child views from RenderedSegment data. The root document and every + * BlockquoteContainerView supply their own implementation so the reconcile / + * layout loop can stay identical while the actual child view construction + * (callbacks, styling, accessibility) varies per host. + */ +interface SegmentViewFactory { + fun matchesKind( + view: View, + segment: RenderedSegment, + ): Boolean + + fun createView(segment: RenderedSegment): View + + fun updateView( + view: View, + segment: RenderedSegment, + ) + + fun animateNewView( + view: View, + segment: RenderedSegment, + ) +} From 2288c216d3ecdc41ab382b68c8ed38f96e377aaf Mon Sep 17 00:00:00 2001 From: Ernest Date: Mon, 17 Aug 2026 13:56:58 +0200 Subject: [PATCH 03/11] feat: added maestro test --- ...ockquote_nested_code_block_combo_test.yaml | 41 ++++++++++++++++++ ...kquote_nested_code_block_combo_display.png | Bin 0 -> 44178 bytes ...kquote_nested_code_block_combo_display.png | Bin 0 -> 44611 bytes 3 files changed, 41 insertions(+) create mode 100644 .maestro/enrichedMarkdownText/flows/advanced/block_elements/blockquote_nested_code_block_combo_test.yaml create mode 100644 .maestro/enrichedMarkdownText/screenshots/android/blockquote_nested_code_block_combo_display.png create mode 100644 .maestro/enrichedMarkdownText/screenshots/ios/blockquote_nested_code_block_combo_display.png diff --git a/.maestro/enrichedMarkdownText/flows/advanced/block_elements/blockquote_nested_code_block_combo_test.yaml b/.maestro/enrichedMarkdownText/flows/advanced/block_elements/blockquote_nested_code_block_combo_test.yaml new file mode 100644 index 000000000..43b38d1b1 --- /dev/null +++ b/.maestro/enrichedMarkdownText/flows/advanced/block_elements/blockquote_nested_code_block_combo_test.yaml @@ -0,0 +1,41 @@ +appId: swmansion.enriched.markdown.example +tags: + - block + - advanced + - blockquote + - code_block + - text +--- +# checks a fenced code block nested INSIDE a blockquote, and inside a nested quote - +# the GFM recursive-container behavior from #608 where a quoted code block becomes a +# real code-block container laid out within the quote's box (not inline text). +- launchApp + +- runFlow: + file: '../../../../subflows/move_to_playground.yaml' + +- runFlow: + file: '../../../subflows/set_enriched_text_value.yaml' + env: + VALUE: | + > Install the package: + > + > ```bash + > npm install my-package + > ``` + > + > then import it. + + > Outer quote + > > Nested quote with a code block: + > > + > > ```js + > > import pkg from 'my-package' + > > ``` + > > + > > done. + +- runFlow: + file: '../../../subflows/capture_or_assert_screenshot.yaml' + env: + SCREENSHOT_NAME: 'blockquote_nested_code_block_combo_display' diff --git a/.maestro/enrichedMarkdownText/screenshots/android/blockquote_nested_code_block_combo_display.png b/.maestro/enrichedMarkdownText/screenshots/android/blockquote_nested_code_block_combo_display.png new file mode 100644 index 0000000000000000000000000000000000000000..0bf1b73b5b9f44eda08374c94566af13099e4912 GIT binary patch literal 44178 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 S>;j=dE$>}>f2iDX^uixT_RVYn literal 0 HcmV?d00001 diff --git a/.maestro/enrichedMarkdownText/screenshots/ios/blockquote_nested_code_block_combo_display.png b/.maestro/enrichedMarkdownText/screenshots/ios/blockquote_nested_code_block_combo_display.png new file mode 100644 index 0000000000000000000000000000000000000000..1cba2d0ed16cd81feeb1b7c70a37f6a1b166c2b2 GIT binary patch literal 44611 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 literal 0 HcmV?d00001 From 61831ab3282c8268fd304d77a6413237b7f9afd0 Mon Sep 17 00:00:00 2001 From: Ernest Date: Mon, 17 Aug 2026 13:57:21 +0200 Subject: [PATCH 04/11] docs: documentation + storybook --- .../block/Blockquote.stories.tsx | 35 ++++++++++++++++--- docs/API_REFERENCE.md | 6 ++-- docs/ELEMENTS_STRUCTURE.md | 2 +- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/apps/react-native-example/.rnstorybook/stories/components/EnrichedMarkdownText/block/Blockquote.stories.tsx b/apps/react-native-example/.rnstorybook/stories/components/EnrichedMarkdownText/block/Blockquote.stories.tsx index 88ffa4356..2b79094c8 100644 --- a/apps/react-native-example/.rnstorybook/stories/components/EnrichedMarkdownText/block/Blockquote.stories.tsx +++ b/apps/react-native-example/.rnstorybook/stories/components/EnrichedMarkdownText/block/Blockquote.stories.tsx @@ -5,6 +5,7 @@ import { blockquoteStyledDefaults, fontFamilyControl, fontWeightControl, + githubFlavorArgTypes, type BlockquoteStyleControls, numberControl, } from '../shared/storybookMarkdownStyles'; @@ -22,6 +23,14 @@ const NESTED_MARKDOWN = `> top-level blockquote >> nested blockquote inside the first >>> deeply nested blockquote`; +const CODE_BLOCK_MARKDOWN = `> Blockquote with a fenced code block inside: +> +> \`\`\`ts +> const answer = 42; +> \`\`\` +> +> and a trailing paragraph.`; + const argTypes = { fontSize: numberControl('markdownStyle.blockquote.fontSize', { min: 12, @@ -95,9 +104,13 @@ function renderBlockquote( ); } +const flavorArgTypes = githubFlavorArgTypes( + 'commonmark renders quotes inline via spans; github renders each quote as a recursive container view (its own padding/background, nested code blocks become real code-block containers).' +); + const blockquoteStoryBase = { - argTypes, - args: blockquoteStyledDefaults, + argTypes: { ...argTypes, ...flavorArgTypes }, + args: { ...blockquoteStyledDefaults, flavor: 'github' as const }, }; export default storyMeta('Block', 'Blockquote'); @@ -111,7 +124,7 @@ export const Default: TextStory = { render: (args) => renderBlockquote( 'Blockquote', - 'Lines prefixed with >. Use the controls to tune markdownStyle.blockquote.', + 'Lines prefixed with >. Flip the flavor control between commonmark and github to compare the inline (span) and container renderers. Use the controls to tune markdownStyle.blockquote.', args ), }; @@ -125,7 +138,21 @@ export const Nested: TextStory = { render: (args) => renderBlockquote( 'Nested Blockquote', - 'Nest blockquotes with multiple > markers. Use the controls to tune markdownStyle.blockquote.', + 'Nest blockquotes with multiple > markers. Flip flavor to compare commonmark (inline spans) with github (one recursive container box per level). Use the controls to tune markdownStyle.blockquote.', + args + ), +}; + +export const WithCodeBlock: TextStory = { + ...blockquoteStoryBase, + args: { + ...blockquoteStoryBase.args, + markdown: CODE_BLOCK_MARKDOWN, + }, + render: (args) => + renderBlockquote( + 'Blockquote with Code Block', + 'A fenced code block inside a blockquote. With flavor="github" the code block becomes a real code-block container nested in the quote; with flavor="commonmark" it renders inline. Use the controls to tune markdownStyle.blockquote.', args ), }; diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index bb188c71d..2818eec7c 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -285,7 +285,7 @@ Code blocks are always rendered left-to-right regardless of this prop. Per-parag ### `flavor` -Markdown flavor. Set to `'github'` to enable GitHub Flavored Markdown features: tables and block-style code blocks. +Markdown flavor. Set to `'github'` to enable GitHub Flavored Markdown features: tables, block-style code blocks, and block-style blockquotes. | Type | Default Value | Platform | | --------------------------------- | --------------- | -------- | @@ -293,9 +293,11 @@ Markdown flavor. Set to `'github'` to enable GitHub Flavored Markdown features: > **Note:** > - **`'commonmark'`**: All Markdown content is rendered as a single TextView. Selecting text will select all content in the view. -> - **`'github'`**: The Markdown AST is split into segments. Consecutive text blocks (paragraphs, headings, lists, etc.) are grouped into separate TextView segments, while tables, fenced code blocks, and math blocks are rendered as separate block views. This allows for granular text selection within each segment and enables interactive block features (horizontal table scrolling, context menus, the code block header). Text selection cannot span across segments. +> - **`'github'`**: The Markdown AST is split into segments. Consecutive text blocks (paragraphs, headings, lists, etc.) are grouped into separate TextView segments, while tables, fenced code blocks, math blocks, and blockquotes are rendered as separate block views. This allows for granular text selection within each segment and enables interactive block features (horizontal table scrolling, context menus, the code block header). Text selection cannot span across segments. > > With `'github'`, a fenced code block renders as a dedicated component: a header bar with the language display name (` ```python ` shows "Python") and a copy-code button, a divider, and the code below. Long lines do not wrap — the code pane scrolls horizontally while the header stays fixed. Long-pressing the block opens the Copy / Copy as Markdown menu. With `'commonmark'`, code blocks stay inside the single TextView, styled via spans, and long lines wrap. +> +> With `'github'`, a blockquote also renders as a dedicated component — a recursive container that draws its own box (padding, `backgroundColor`, and accent border with `borderRadius`) and splits its own content into segments. A code block, table, or math block quoted inside therefore becomes a real nested block component, and each nesting level is its own box. With `'commonmark'`, blockquotes stay inside the single TextView, styled via spans. ### `streamingAnimation` diff --git a/docs/ELEMENTS_STRUCTURE.md b/docs/ELEMENTS_STRUCTURE.md index b40c8344e..014adb066 100644 --- a/docs/ELEMENTS_STRUCTURE.md +++ b/docs/ELEMENTS_STRUCTURE.md @@ -12,7 +12,7 @@ Markdown elements in `react-native-enriched-markdown` are organized into block a |---------|--------|----------------|-------------| | Headings | `# H1` to `###### H6` | `h1` - `h6` | Six levels of headings | | Paragraphs | Plain text | `paragraph` | Default text container | -| Blockquotes | `> Quote` | `blockquote` | Quoted content with accent bar, unlimited nesting | +| Blockquotes | `> Quote` | `blockquote` | Quoted content with accent bar, unlimited nesting; with `flavor="github"` rendered as a recursive block container (own padding/background box, and quoted code blocks/tables become real nested block components) | | Code Blocks | ` ``` code ``` ` | `codeBlock` | Multi-line code containers; with `flavor="github"` rendered as a block component with a language header and copy-code button | | Unordered Lists | `- Item`, `* Item`, or `+ Item` | `list` | Bullet lists with unlimited nesting | | Ordered Lists | `1. Item` | `list` | Numbered lists with unlimited nesting | From 0a2d0cd1dd11c27ca8b5ff90f3080674926ae844 Mon Sep 17 00:00:00 2001 From: Ernest Date: Mon, 17 Aug 2026 13:57:43 +0200 Subject: [PATCH 05/11] feat: web --- packages/react-native-enriched-markdown/src/web/styles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-enriched-markdown/src/web/styles.ts b/packages/react-native-enriched-markdown/src/web/styles.ts index 8f9e11cc8..c75dca604 100644 --- a/packages/react-native-enriched-markdown/src/web/styles.ts +++ b/packages/react-native-enriched-markdown/src/web/styles.ts @@ -125,7 +125,7 @@ function blockquoteStyle(style: MarkdownStyleInternal): CSSProperties { marginBottom: blockquote.marginBottom, marginInlineStart: 0, // reset UA default (40px in LTR, auto in RTL) marginInlineEnd: 0, - paddingInlineStart: blockquote.gapWidth, + paddingInlineStart: blockquote.gapWidth + blockquote.padding, paddingInlineEnd: blockquote.padding, paddingTop: blockquote.padding, paddingBottom: blockquote.padding, From cea16a336e5795cfbbb2f3c404e8f7cb48e18614 Mon Sep 17 00:00:00 2001 From: Ernest Date: Tue, 18 Aug 2026 16:20:52 +0200 Subject: [PATCH 06/11] feat: review1 - changed the placement of blcockquote attributed string renderer --- .../renderer/BlockquoteTextRenderer.kt | 46 ++++++++++++ .../markdown/renderer/DocumentRenderer.kt | 16 ----- .../markdown/renderer/NodeRenderer.kt | 20 +++++- .../enriched/markdown/renderer/Renderer.kt | 72 +++++-------------- .../markdown/utils/common/RenderedSegment.kt | 6 +- .../markdown/views/TableContainerView.kt | 6 +- .../ios/renderer/AttributedRenderer.h | 17 ++--- .../ios/renderer/AttributedRenderer.m | 48 ++++++------- .../ios/renderer/ENRMBlockquoteTextRenderer.h | 25 +++++++ .../ios/renderer/ENRMBlockquoteTextRenderer.m | 34 +++++++++ .../ios/utils/ENRMTextRenderer.h | 2 +- .../ios/utils/ENRMTextRenderer.m | 25 ++----- .../ios/views/TableContainerView.m | 7 +- 13 files changed, 184 insertions(+), 140 deletions(-) create mode 100644 packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteTextRenderer.kt delete mode 100644 packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/DocumentRenderer.kt create mode 100644 packages/react-native-enriched-markdown/ios/renderer/ENRMBlockquoteTextRenderer.h create mode 100644 packages/react-native-enriched-markdown/ios/renderer/ENRMBlockquoteTextRenderer.m diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteTextRenderer.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteTextRenderer.kt new file mode 100644 index 000000000..196b36eab --- /dev/null +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteTextRenderer.kt @@ -0,0 +1,46 @@ +package com.swmansion.enriched.markdown.renderer + +import android.text.SpannableString +import android.text.SpannableStringBuilder +import com.swmansion.enriched.markdown.parser.MarkdownASTNode +import com.swmansion.enriched.markdown.styles.BlockquoteStyle +import com.swmansion.enriched.markdown.utils.text.span.applyLineHeightSkippingImages + +/** + * Renders a GFM blockquote's own prose content - the nodes left after nested + * quotes, code blocks and other block segments have been split out - into a + * standalone SpannableString styled as blockquote text. + * + * It decorates a generic [Renderer.renderContent] pass rather than owning the + * render envelope itself: [push] enters the blockquote block style so text picks + * up the quote's font/color and paragraphs render tight (isInsideBlockElement), + * matching the commonmark BlockquoteRenderer; [postProcess] applies the quote's + * line height once the builder is complete. It draws no border/background/padding + * - the BlockquoteContainerView draws the box. + */ +class BlockquoteTextRenderer( + private val style: BlockquoteStyle, +) { + fun render( + renderer: Renderer, + nodes: List, + onLinkPress: ((String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + ): SpannableString = renderer.renderContent(nodes, onLinkPress, onLinkLongPress, block = this) + + fun push(context: BlockStyleContext) { + context.blockquoteDepth += 1 + context.setBlockquoteStyle(style) + } + + fun pop(context: BlockStyleContext) { + context.popBlockStyle() + context.blockquoteDepth -= 1 + } + + fun postProcess(builder: SpannableStringBuilder) { + if (builder.isNotEmpty()) { + applyLineHeightSkippingImages(builder, 0, builder.length, style.lineHeight) + } + } +} diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/DocumentRenderer.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/DocumentRenderer.kt deleted file mode 100644 index f6ad6a24e..000000000 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/DocumentRenderer.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.swmansion.enriched.markdown.renderer - -import android.text.SpannableStringBuilder -import com.swmansion.enriched.markdown.parser.MarkdownASTNode - -class DocumentRenderer : NodeRenderer { - override fun render( - node: MarkdownASTNode, - builder: SpannableStringBuilder, - onLinkPress: ((String) -> Unit)?, - onLinkLongPress: ((String) -> Unit)?, - factory: RendererFactory, - ) { - factory.renderChildren(node, builder, onLinkPress, onLinkLongPress) - } -} diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt index d51b7cef5..e028f9fe4 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt @@ -84,7 +84,6 @@ class RendererFactory( private val renderers: Map by lazy { buildMap { - put(MarkdownASTNode.NodeType.Document, DocumentRenderer()) put(MarkdownASTNode.NodeType.Paragraph, ParagraphRenderer(config)) put(MarkdownASTNode.NodeType.Heading, HeadingRenderer(config)) put(MarkdownASTNode.NodeType.Blockquote, BlockquoteRenderer(config)) @@ -144,8 +143,23 @@ class RendererFactory( onLinkPress: ((String) -> Unit)?, onLinkLongPress: ((String) -> Unit)?, ) { - node.children.forEach { child -> - getRenderer(child).render(child, builder, onLinkPress, onLinkLongPress, this) + renderNodes(node.children, builder, onLinkPress, onLinkLongPress) + } + + /** + * Renders a flat list of sibling nodes in order, dispatching each to its + * NodeRenderer. Renders a block's own child content directly - e.g. a GFM + * blockquote's content once its nested block segments have been split out - + * without wrapping it in a synthetic root node. + */ + fun renderNodes( + nodes: List, + builder: SpannableStringBuilder, + onLinkPress: ((String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + ) { + nodes.forEach { node -> + getRenderer(node).render(node, builder, onLinkPress, onLinkLongPress, this) } } diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/Renderer.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/Renderer.kt index 0aae10175..56e61a522 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/Renderer.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/Renderer.kt @@ -6,9 +6,7 @@ import android.text.SpannableStringBuilder import com.swmansion.enriched.markdown.parser.MarkdownASTNode import com.swmansion.enriched.markdown.spans.ImageSpan import com.swmansion.enriched.markdown.spans.MarginBottomSpan -import com.swmansion.enriched.markdown.styles.BlockquoteStyle import com.swmansion.enriched.markdown.styles.StyleConfig -import com.swmansion.enriched.markdown.utils.text.span.applyLineHeightSkippingImages class Renderer { private var cachedFactory: RendererFactory? = null @@ -37,47 +35,29 @@ class Renderer { document: MarkdownASTNode, onLinkPress: ((String) -> Unit)? = null, onLinkLongPress: ((String) -> Unit)? = null, - ): SpannableString { - val factory = - requireNotNull(cachedFactory) { - "Renderer must be configured with a style before calling renderDocument." - } - - factory.resetForNewRender() - collectedImageSpans.clear() - lastElementMarginBottom = 0f - - val builder = SpannableStringBuilder() - - renderNode(document, builder, onLinkPress, onLinkLongPress, factory) - - // Remove trailing margin from last block element - removeTrailingMargin(builder) - - // Flush deferred spans (e.g. BaselineShiftSpan) after all block-level spans are set. - // See BaselineShiftRenderer for context and the proper long-term fix. - factory.flushDeferredSpans(builder) - - return SpannableString(builder) - } + ): SpannableString = renderContent(document.children, onLinkPress, onLinkLongPress) /** - * Renders a blockquote's own content (the nodes left after nested quotes, code - * blocks and other block segments have been split out) styled as blockquote - * text: the blockquote block style is pushed so text picks up the quote's - * font/color and paragraphs render tight (isInsideBlockElement), matching the - * commonmark BlockquoteRenderer. It applies the quote's line height but draws no - * border/background/padding - the BlockquoteContainerView draws the box. + * Renders a flat list of sibling nodes into a standalone SpannableString. This + * is the generic envelope shared by the document and block-segment paths: reset + * the factory, build the spannable, trim the trailing margin, and flush deferred + * spans (e.g. BaselineShiftSpan) after all block-level spans are set - see + * BaselineShiftRenderer for the proper long-term fix. + * + * An optional [block] decorates the pass - it enters a block style before the + * nodes render and post-processes the finished builder - letting a caller render + * content as e.g. blockquote text without this class knowing anything + * block-specific. See [BlockquoteTextRenderer]. */ - fun renderBlockquoteContent( + fun renderContent( nodes: List, - blockquoteStyle: BlockquoteStyle, onLinkPress: ((String) -> Unit)? = null, onLinkLongPress: ((String) -> Unit)? = null, + block: BlockquoteTextRenderer? = null, ): SpannableString { val factory = requireNotNull(cachedFactory) { - "Renderer must be configured with a style before calling renderBlockquoteContent." + "Renderer must be configured with a style before rendering." } factory.resetForNewRender() @@ -85,22 +65,16 @@ class Renderer { lastElementMarginBottom = 0f val builder = SpannableStringBuilder() - val document = MarkdownASTNode(type = MarkdownASTNode.NodeType.Document, children = nodes) - val context = factory.blockStyleContext - context.blockquoteDepth += 1 - context.setBlockquoteStyle(blockquoteStyle) + block?.push(factory.blockStyleContext) try { - renderNode(document, builder, onLinkPress, onLinkLongPress, factory) + factory.renderNodes(nodes, builder, onLinkPress, onLinkLongPress) } finally { - context.popBlockStyle() - context.blockquoteDepth -= 1 + block?.pop(factory.blockStyleContext) } removeTrailingMargin(builder) - if (builder.isNotEmpty()) { - applyLineHeightSkippingImages(builder, 0, builder.length, blockquoteStyle.lineHeight) - } + block?.postProcess(builder) factory.flushDeferredSpans(builder) return SpannableString(builder) @@ -136,16 +110,6 @@ class Renderer { */ fun getLastElementMarginBottom(): Float = lastElementMarginBottom - private fun renderNode( - node: MarkdownASTNode, - builder: SpannableStringBuilder, - onLinkPress: ((String) -> Unit)?, - onLinkLongPress: ((String) -> Unit)?, - factory: RendererFactory, - ) { - factory.getRenderer(node).render(node, builder, onLinkPress, onLinkLongPress, factory) - } - /** * Internal helper used by the Factory's lambda to collect spans. */ diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/RenderedSegment.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/RenderedSegment.kt index 4880e7e24..9604dd04c 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/RenderedSegment.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/RenderedSegment.kt @@ -3,6 +3,7 @@ package com.swmansion.enriched.markdown.utils.common import android.content.Context import android.text.SpannableString import com.swmansion.enriched.markdown.parser.MarkdownASTNode +import com.swmansion.enriched.markdown.renderer.BlockquoteTextRenderer import com.swmansion.enriched.markdown.renderer.Renderer import com.swmansion.enriched.markdown.spans.ImageSpan import com.swmansion.enriched.markdown.styles.BlockquoteStyle @@ -91,10 +92,9 @@ object MarkdownSegmentRenderer { val styledText = if (blockquoteStyle != null) { - renderer.renderBlockquoteContent(nodes, blockquoteStyle, onLinkPress, onLinkLongPress) + BlockquoteTextRenderer(blockquoteStyle).render(renderer, nodes, onLinkPress, onLinkLongPress) } else { - val documentWrapper = MarkdownASTNode(type = MarkdownASTNode.NodeType.Document, children = nodes) - renderer.renderDocument(documentWrapper, onLinkPress, onLinkLongPress) + renderer.renderContent(nodes, onLinkPress, onLinkLongPress) } return RenderedSegment.Text( diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt index 736a0e8d0..97a35485a 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt @@ -152,11 +152,11 @@ class TableContainerView( isHeader: Boolean, alignment: Layout.Alignment, ): SpannableString { - val root = MarkdownASTNode(NodeType.Document, children = listOf(MarkdownASTNode(NodeType.Paragraph, children = node.children))) + val paragraph = MarkdownASTNode(NodeType.Paragraph, children = node.children) val cellParagraphStyle = styleConfig.tableCellParagraphStyle(tableStyle, isHeader) return styleConfig .withParagraphOverride(cellParagraphStyle) { - Renderer().apply { configure(styleConfig, context) }.renderDocument(root, onLinkPress, onLinkLongPress) + Renderer().apply { configure(styleConfig, context) }.renderContent(listOf(paragraph), onLinkPress, onLinkLongPress) }.apply { if (isNotEmpty()) { if (isHeader) setSpan(HeaderTypefaceSpan(styleConfig.tableHeaderTypeface ?: Typeface.DEFAULT_BOLD), 0, length, 33) @@ -479,7 +479,7 @@ class TableContainerView( config.withParagraphOverride(cellParagraphStyle) { Renderer() .apply { configure(config, context) } - .renderDocument(MarkdownASTNode(NodeType.Document, children = listOf(paragraph)), null, null) + .renderContent(listOf(paragraph), null, null) } styledText.replaceMathSpansWithPlaceholders(context) if (isHeader && styledText.isNotEmpty()) { diff --git a/packages/react-native-enriched-markdown/ios/renderer/AttributedRenderer.h b/packages/react-native-enriched-markdown/ios/renderer/AttributedRenderer.h index b0397e50a..0b0bdbbbf 100644 --- a/packages/react-native-enriched-markdown/ios/renderer/AttributedRenderer.h +++ b/packages/react-native-enriched-markdown/ios/renderer/AttributedRenderer.h @@ -1,18 +1,19 @@ #import +@class ENRMBlockquoteTextRenderer; @class MarkdownASTNode; @class RenderContext; @interface AttributedRenderer : NSObject - (instancetype)initWithConfig:(id)config; -- (NSMutableAttributedString *)renderRoot:(MarkdownASTNode *)root context:(RenderContext *)context; -// Renders a blockquote's own content (the nodes left after nested quotes/code blocks are split -// out) with a blockquote baseline block style instead of paragraph: text picks up the quote's -// font/color and paragraphs render tight (no paragraph margins), matching the commonmark path. -// It draws no box; the ENRMBlockquoteContainerView draws the border/background/padding. -- (NSMutableAttributedString *)renderRoot:(MarkdownASTNode *)root - context:(RenderContext *)context - asBlockquoteContent:(BOOL)asBlockquoteContent; +// Renders a flat list of sibling nodes into an attributed string: sets the baseline block style, +// walks each node, and trims the trailing block margin. Pass a non-nil block to render the nodes as +// a blockquote's own content (blockquote baseline + the quote's line height applied over the run); +// it draws no box - the ENRMBlockquoteContainerView draws the border/background/padding. See +// ENRMBlockquoteTextRenderer. +- (NSMutableAttributedString *)renderNodes:(NSArray *)nodes + context:(RenderContext *)context + block:(nullable ENRMBlockquoteTextRenderer *)block; - (CGFloat)getLastElementMarginBottom; - (void)setAllowTrailingMargin:(BOOL)allow; @end diff --git a/packages/react-native-enriched-markdown/ios/renderer/AttributedRenderer.m b/packages/react-native-enriched-markdown/ios/renderer/AttributedRenderer.m index 34706371a..519d37a00 100644 --- a/packages/react-native-enriched-markdown/ios/renderer/AttributedRenderer.m +++ b/packages/react-native-enriched-markdown/ios/renderer/AttributedRenderer.m @@ -1,6 +1,7 @@ #import "AttributedRenderer.h" #import "BlockquoteBorder.h" #import "CodeBlockBackground.h" +#import "ENRMBlockquoteTextRenderer.h" #import "LastElementUtils.h" #import "MarkdownASTNode.h" #import "NodeRenderer.h" @@ -29,40 +30,30 @@ - (instancetype)initWithConfig:(StyleConfig *)config } /** - * Entry point for rendering a Markdown AST. - * Sets the baseline global style and initiates the recursive traversal. + * Entry point for rendering a flat list of sibling nodes (e.g. a document's or a block segment's + * children). Sets the baseline block style, runs the recursive traversal, and trims the trailing + * block margin. An optional [block] decorates the pass - it enters a block baseline before the nodes + * render and post-processes the finished string - letting a caller render content as e.g. blockquote + * text without this class knowing anything block-specific. See ENRMBlockquoteTextRenderer. */ -- (NSMutableAttributedString *)renderRoot:(MarkdownASTNode *)root context:(RenderContext *)context +- (NSMutableAttributedString *)renderNodes:(NSArray *)nodes + context:(RenderContext *)context + block:(ENRMBlockquoteTextRenderer *)block { - return [self renderRoot:root context:context asBlockquoteContent:NO]; -} - -- (NSMutableAttributedString *)renderRoot:(MarkdownASTNode *)root - context:(RenderContext *)context - asBlockquoteContent:(BOOL)asBlockquoteContent -{ - if (!root) - return [[NSMutableAttributedString alloc] init]; - - // 1. Establish the global baseline style. - // This ensures that leaf nodes (like Text) have valid attributes if they appear at the root. - // A blockquote's own content uses the blockquote baseline so its paragraphs render tight and - // pick up the quote's font/color (currentBlockType == Blockquote), mirroring commonmark. - if (asBlockquoteContent) { - context.blockquoteDepth = context.blockquoteDepth + 1; - [context setBlockStyle:BlockTypeBlockquote - font:_config.blockquoteFont - color:_config.blockquoteColor - headingLevel:0]; + // 1. Establish the baseline block style. This ensures leaf nodes (like Text) have valid + // attributes if they appear at the top level. A decorator overrides the default paragraph + // baseline (e.g. the blockquote baseline so paragraphs render tight and pick up the quote's + // font/color). + if (block) { + [block pushOnContext:context]; } else { [context setBlockStyle:BlockTypeParagraph font:_config.paragraphFont color:_config.paragraphColor headingLevel:0]; } NSMutableAttributedString *output = [[NSMutableAttributedString alloc] init]; - // 2. Iterate through root children. - // We skip the 'Root' node itself as it is a container, not a renderable element. - for (MarkdownASTNode *node in root.children) { + // 2. Iterate through the sibling nodes. + for (MarkdownASTNode *node in nodes) { [self renderNodeRecursive:node into:output context:context]; } @@ -71,7 +62,10 @@ - (NSMutableAttributedString *)renderRoot:(MarkdownASTNode *)root _lastElementMarginBottom = 0.0; [self removeTrailingSpacing:output]; - // 4. Cleanup global state to prevent side effects in subsequent renders. + // 4. Let the decorator post-process the finished string (e.g. stamp the quote's line height). + [block postProcess:output]; + + // 5. Cleanup global state to prevent side effects in subsequent renders. [context clearBlockStyle]; return output; diff --git a/packages/react-native-enriched-markdown/ios/renderer/ENRMBlockquoteTextRenderer.h b/packages/react-native-enriched-markdown/ios/renderer/ENRMBlockquoteTextRenderer.h new file mode 100644 index 000000000..f371dd571 --- /dev/null +++ b/packages/react-native-enriched-markdown/ios/renderer/ENRMBlockquoteTextRenderer.h @@ -0,0 +1,25 @@ +#pragma once +#import + +@class RenderContext; +@class StyleConfig; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Renders a GFM blockquote's own prose content - the nodes left after nested quotes, code blocks and + * other block segments have been split out - as blockquote text. + * + * It decorates a generic AttributedRenderer pass rather than owning the render envelope itself: + * -pushOnContext: enters the blockquote baseline block style (+depth) so text picks up the quote's + * font/color and paragraphs render tight (currentBlockType == Blockquote), matching the commonmark + * path; -postProcess: applies the quote's line height once the string is complete. It draws no + * border/background/padding - the ENRMBlockquoteContainerView draws the box. + */ +@interface ENRMBlockquoteTextRenderer : NSObject +- (instancetype)initWithConfig:(StyleConfig *)config; +- (void)pushOnContext:(RenderContext *)context; +- (void)postProcess:(NSMutableAttributedString *)text; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native-enriched-markdown/ios/renderer/ENRMBlockquoteTextRenderer.m b/packages/react-native-enriched-markdown/ios/renderer/ENRMBlockquoteTextRenderer.m new file mode 100644 index 000000000..0cda58cd4 --- /dev/null +++ b/packages/react-native-enriched-markdown/ios/renderer/ENRMBlockquoteTextRenderer.m @@ -0,0 +1,34 @@ +#import "ENRMBlockquoteTextRenderer.h" +#import "ParagraphStyleUtils.h" +#import "RenderContext.h" +#import "StyleConfig.h" + +@implementation ENRMBlockquoteTextRenderer { + StyleConfig *_config; +} + +- (instancetype)initWithConfig:(StyleConfig *)config +{ + self = [super init]; + if (self) { + _config = config; + } + return self; +} + +- (void)pushOnContext:(RenderContext *)context +{ + context.blockquoteDepth = context.blockquoteDepth + 1; + [context setBlockStyle:BlockTypeBlockquote font:_config.blockquoteFont color:_config.blockquoteColor headingLevel:0]; +} + +- (void)postProcess:(NSMutableAttributedString *)text +{ + // Paragraphs apply their own line height regardless of block type; a quote's own line height is + // stamped over the whole run here, matching Android's BlockquoteTextRenderer. + if (text.length > 0) { + applyLineHeight(text, NSMakeRange(0, text.length), [_config blockquoteLineHeight]); + } +} + +@end diff --git a/packages/react-native-enriched-markdown/ios/utils/ENRMTextRenderer.h b/packages/react-native-enriched-markdown/ios/utils/ENRMTextRenderer.h index b78f207b6..6b426af76 100644 --- a/packages/react-native-enriched-markdown/ios/utils/ENRMTextRenderer.h +++ b/packages/react-native-enriched-markdown/ios/utils/ENRMTextRenderer.h @@ -26,7 +26,7 @@ ENRMRenderResult *ENRMRenderASTNodes(NSArray *nodes, StyleCon // Renders a blockquote's own text content: like ENRMRenderASTNodes but with a blockquote baseline // (tight paragraphs, quote font/color) and the quote's line height applied over the result. Draws // no box - the ENRMBlockquoteContainerView draws the border/background/padding. Mirrors Android's -// Renderer.renderBlockquoteContent. +// BlockquoteTextRenderer. ENRMRenderResult *ENRMRenderBlockquoteContentNodes(NSArray *nodes, StyleConfig *config, BOOL allowFontScaling, CGFloat maxFontSizeMultiplier, NSLineBreakStrategy lineBreakStrategy); diff --git a/packages/react-native-enriched-markdown/ios/utils/ENRMTextRenderer.m b/packages/react-native-enriched-markdown/ios/utils/ENRMTextRenderer.m index 49a175437..b9a31e55e 100644 --- a/packages/react-native-enriched-markdown/ios/utils/ENRMTextRenderer.m +++ b/packages/react-native-enriched-markdown/ios/utils/ENRMTextRenderer.m @@ -1,8 +1,7 @@ #import "ENRMTextRenderer.h" #import "AccessibilityInfo.h" #import "AttributedRenderer.h" -#import "MarkdownASTNode.h" -#import "ParagraphStyleUtils.h" +#import "ENRMBlockquoteTextRenderer.h" #import "RenderContext.h" #import "StyleConfig.h" @@ -12,13 +11,8 @@ @implementation ENRMRenderResult static ENRMRenderResult *ENRMRenderASTNodesCore(NSArray *nodes, StyleConfig *config, BOOL allowTrailingMargin, BOOL allowFontScaling, CGFloat maxFontSizeMultiplier, NSLineBreakStrategy lineBreakStrategy, - BOOL asBlockquoteContent) + ENRMBlockquoteTextRenderer *block) { - MarkdownASTNode *root = [[MarkdownASTNode alloc] initWithType:MarkdownNodeTypeDocument]; - for (MarkdownASTNode *node in nodes) { - [root addChild:node]; - } - AttributedRenderer *renderer = [[AttributedRenderer alloc] initWithConfig:config]; [renderer setAllowTrailingMargin:allowTrailingMargin]; @@ -26,15 +20,7 @@ @implementation ENRMRenderResult context.allowFontScaling = allowFontScaling; context.maxFontSizeMultiplier = maxFontSizeMultiplier; - NSMutableAttributedString *attributedText = [renderer renderRoot:root - context:context - asBlockquoteContent:asBlockquoteContent]; - - // Paragraphs apply the paragraph line height regardless of block type; a quote's own line - // height is stamped over the whole run here, matching Renderer.renderBlockquoteContent on Android. - if (asBlockquoteContent && attributedText.length > 0) { - applyLineHeight(attributedText, NSMakeRange(0, attributedText.length), [config blockquoteLineHeight]); - } + NSMutableAttributedString *attributedText = [renderer renderNodes:nodes context:context block:block]; [context applyLinkAttributesToString:attributedText]; ENRMApplyLineBreakStrategyToParagraphStyles(attributedText, lineBreakStrategy); @@ -52,13 +38,14 @@ @implementation ENRMRenderResult NSLineBreakStrategy lineBreakStrategy) { return ENRMRenderASTNodesCore(nodes, config, allowTrailingMargin, allowFontScaling, maxFontSizeMultiplier, - lineBreakStrategy, /*asBlockquoteContent*/ NO); + lineBreakStrategy, /*block*/ nil); } ENRMRenderResult *ENRMRenderBlockquoteContentNodes(NSArray *nodes, StyleConfig *config, BOOL allowFontScaling, CGFloat maxFontSizeMultiplier, NSLineBreakStrategy lineBreakStrategy) { + ENRMBlockquoteTextRenderer *block = [[ENRMBlockquoteTextRenderer alloc] initWithConfig:config]; return ENRMRenderASTNodesCore(nodes, config, /*allowTrailingMargin*/ NO, allowFontScaling, maxFontSizeMultiplier, - lineBreakStrategy, /*asBlockquoteContent*/ YES); + lineBreakStrategy, block); } diff --git a/packages/react-native-enriched-markdown/ios/views/TableContainerView.m b/packages/react-native-enriched-markdown/ios/views/TableContainerView.m index b76936d86..326e240b8 100644 --- a/packages/react-native-enriched-markdown/ios/views/TableContainerView.m +++ b/packages/react-native-enriched-markdown/ios/views/TableContainerView.m @@ -77,17 +77,12 @@ static NSTextAlignment ENRMTableTextAlignmentFromString(NSString *align) ENRMWritingDirectionMode writingDirectionMode, NSWritingDirection resolvedLayoutDirection) { - MarkdownASTNode *temporaryRoot = [[MarkdownASTNode alloc] initWithType:MarkdownNodeTypeDocument]; - for (MarkdownASTNode *child in cellNode.children) { - [temporaryRoot addChild:child]; - } - AttributedRenderer *renderer = [[AttributedRenderer alloc] initWithConfig:cellConfig]; RenderContext *context = [RenderContext new]; context.allowFontScaling = allowFontScaling; context.maxFontSizeMultiplier = maxFontSizeMultiplier; - NSMutableAttributedString *attributedText = [renderer renderRoot:temporaryRoot context:context]; + NSMutableAttributedString *attributedText = [renderer renderNodes:cellNode.children context:context block:nil]; [context applyLinkAttributesToString:attributedText]; From 15e26680fd738ce79cbca83b6f8be086fd2837bc Mon Sep 17 00:00:00 2001 From: Ernest Date: Tue, 18 Aug 2026 17:30:49 +0200 Subject: [PATCH 07/11] refactor: moved segments logic to separate feature directory --- .../android/src/main/baseline-prof.txt | 43 +++++++++---------- .../enriched/markdown/EnrichedMarkdown.kt | 20 ++++----- .../markdown/EnrichedMarkdownInternalText.kt | 2 +- .../enriched/markdown/MeasurementStore.kt | 12 +++--- .../{views => segments}/BlockSegmentView.kt | 2 +- .../BlockquoteContainerView.kt | 5 +-- .../CodeBlockContainerView.kt | 4 +- .../{views => segments}/ContainerNodeView.kt | 4 +- .../common => segments}/MarkdownSegment.kt | 2 +- .../common => segments}/RenderedSegment.kt | 2 +- .../SegmentHeightMeasurer.kt | 3 +- .../common => segments}/SegmentReconciler.kt | 2 +- .../common => segments}/SegmentSignature.kt | 2 +- .../SegmentViewCreators.kt | 5 +-- .../{views => segments}/SegmentViewFactory.kt | 3 +- .../{views => segments}/TableContainerView.kt | 2 +- .../{views => segments}/MathContainerView.kt | 3 +- .../ENRMBlockquoteContainerView.h | 0 .../ENRMBlockquoteContainerView.mm | 0 .../ENRMCodeBlockContainerView.h | 0 .../ENRMCodeBlockContainerView.m | 0 .../ENRMContainerNodeView.h | 0 .../ENRMContainerNodeView.mm | 0 .../ENRMMathContainerView.h | 0 .../ENRMMathContainerView.m | 0 .../ENRMSegmentHeightMeasurer.h | 0 .../ENRMSegmentHeightMeasurer.mm | 0 .../{views => segments}/ENRMTableGridView.h | 0 .../{views => segments}/ENRMTableGridView.m | 0 .../ENRMTableIOSGridView.h | 0 .../ENRMTableIOSGridView.m | 0 .../RenderedMarkdownSegment.h | 0 .../RenderedMarkdownSegment.m | 0 .../{utils => segments}/SegmentReconciler.h | 0 .../{utils => segments}/SegmentReconciler.m | 0 .../ios/{utils => segments}/SegmentRenderer.h | 0 .../ios/{utils => segments}/SegmentRenderer.m | 0 .../{utils => segments}/SegmentViewRegistry.h | 0 .../{utils => segments}/SegmentViewRegistry.m | 0 .../{views => segments}/TableContainerView.h | 0 .../{views => segments}/TableContainerView.m | 0 41 files changed, 55 insertions(+), 61 deletions(-) rename packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/{views => segments}/BlockSegmentView.kt (82%) rename packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/{views => segments}/BlockquoteContainerView.kt (97%) rename packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/{views => segments}/CodeBlockContainerView.kt (98%) rename packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/{views => segments}/ContainerNodeView.kt (96%) rename packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/{utils/common => segments}/MarkdownSegment.kt (97%) rename packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/{utils/common => segments}/RenderedSegment.kt (98%) rename packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/{views => segments}/SegmentHeightMeasurer.kt (97%) rename packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/{utils/common => segments}/SegmentReconciler.kt (98%) rename packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/{utils/common => segments}/SegmentSignature.kt (97%) rename packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/{views => segments}/SegmentViewCreators.kt (97%) rename packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/{views => segments}/SegmentViewFactory.kt (85%) rename packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/{views => segments}/TableContainerView.kt (99%) rename packages/react-native-enriched-markdown/android/src/math/java/com/swmansion/enriched/markdown/{views => segments}/MathContainerView.kt (97%) rename packages/react-native-enriched-markdown/ios/{views => segments}/ENRMBlockquoteContainerView.h (100%) rename packages/react-native-enriched-markdown/ios/{views => segments}/ENRMBlockquoteContainerView.mm (100%) rename packages/react-native-enriched-markdown/ios/{views => segments}/ENRMCodeBlockContainerView.h (100%) rename packages/react-native-enriched-markdown/ios/{views => segments}/ENRMCodeBlockContainerView.m (100%) rename packages/react-native-enriched-markdown/ios/{views => segments}/ENRMContainerNodeView.h (100%) rename packages/react-native-enriched-markdown/ios/{views => segments}/ENRMContainerNodeView.mm (100%) rename packages/react-native-enriched-markdown/ios/{views => segments}/ENRMMathContainerView.h (100%) rename packages/react-native-enriched-markdown/ios/{views => segments}/ENRMMathContainerView.m (100%) rename packages/react-native-enriched-markdown/ios/{utils => segments}/ENRMSegmentHeightMeasurer.h (100%) rename packages/react-native-enriched-markdown/ios/{utils => segments}/ENRMSegmentHeightMeasurer.mm (100%) rename packages/react-native-enriched-markdown/ios/{views => segments}/ENRMTableGridView.h (100%) rename packages/react-native-enriched-markdown/ios/{views => segments}/ENRMTableGridView.m (100%) rename packages/react-native-enriched-markdown/ios/{views => segments}/ENRMTableIOSGridView.h (100%) rename packages/react-native-enriched-markdown/ios/{views => segments}/ENRMTableIOSGridView.m (100%) rename packages/react-native-enriched-markdown/ios/{utils => segments}/RenderedMarkdownSegment.h (100%) rename packages/react-native-enriched-markdown/ios/{utils => segments}/RenderedMarkdownSegment.m (100%) rename packages/react-native-enriched-markdown/ios/{utils => segments}/SegmentReconciler.h (100%) rename packages/react-native-enriched-markdown/ios/{utils => segments}/SegmentReconciler.m (100%) rename packages/react-native-enriched-markdown/ios/{utils => segments}/SegmentRenderer.h (100%) rename packages/react-native-enriched-markdown/ios/{utils => segments}/SegmentRenderer.m (100%) rename packages/react-native-enriched-markdown/ios/{utils => segments}/SegmentViewRegistry.h (100%) rename packages/react-native-enriched-markdown/ios/{utils => segments}/SegmentViewRegistry.m (100%) rename packages/react-native-enriched-markdown/ios/{views => segments}/TableContainerView.h (100%) rename packages/react-native-enriched-markdown/ios/{views => segments}/TableContainerView.m (100%) diff --git a/packages/react-native-enriched-markdown/android/src/main/baseline-prof.txt b/packages/react-native-enriched-markdown/android/src/main/baseline-prof.txt index 675e10168..8a49f91d1 100644 --- a/packages/react-native-enriched-markdown/android/src/main/baseline-prof.txt +++ b/packages/react-native-enriched-markdown/android/src/main/baseline-prof.txt @@ -145,7 +145,6 @@ Lcom/swmansion/enriched/markdown/renderer/BlockquoteRenderer; Lcom/swmansion/enriched/markdown/renderer/CodeBlockRenderer; Lcom/swmansion/enriched/markdown/renderer/CodeBlockRenderer$CodeBlockBoundaryPaddingSpan; Lcom/swmansion/enriched/markdown/renderer/CodeRenderer; -Lcom/swmansion/enriched/markdown/renderer/DocumentRenderer; Lcom/swmansion/enriched/markdown/renderer/EmphasisRenderer; Lcom/swmansion/enriched/markdown/renderer/HeadingRenderer; Lcom/swmansion/enriched/markdown/renderer/HighlightRenderer; @@ -255,22 +254,22 @@ Lcom/swmansion/enriched/markdown/styles/UnderlineStyle; # Utilities - Common Lcom/swmansion/enriched/markdown/utils/common/BreakStrategyUtils; Lcom/swmansion/enriched/markdown/utils/common/FeatureFlags; -Lcom/swmansion/enriched/markdown/utils/common/MarkdownSegment; -Lcom/swmansion/enriched/markdown/utils/common/MarkdownSegment$Math; -Lcom/swmansion/enriched/markdown/utils/common/MarkdownSegment$Table; -Lcom/swmansion/enriched/markdown/utils/common/MarkdownSegment$Text; -Lcom/swmansion/enriched/markdown/utils/common/MarkdownSegmentKt; -Lcom/swmansion/enriched/markdown/utils/common/MarkdownSegmentRenderer; +Lcom/swmansion/enriched/markdown/segments/MarkdownSegment; +Lcom/swmansion/enriched/markdown/segments/MarkdownSegment$Math; +Lcom/swmansion/enriched/markdown/segments/MarkdownSegment$Table; +Lcom/swmansion/enriched/markdown/segments/MarkdownSegment$Text; +Lcom/swmansion/enriched/markdown/segments/MarkdownSegmentKt; +Lcom/swmansion/enriched/markdown/segments/MarkdownSegmentRenderer; Lcom/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtilsKt; Lcom/swmansion/enriched/markdown/utils/common/MotionPreferencesKt; Lcom/swmansion/enriched/markdown/utils/common/ReadableMapExtensionsKt; Lcom/swmansion/enriched/markdown/utils/common/ReconciliationResult; -Lcom/swmansion/enriched/markdown/utils/common/RenderedSegment; -Lcom/swmansion/enriched/markdown/utils/common/RenderedSegment$Math; -Lcom/swmansion/enriched/markdown/utils/common/RenderedSegment$Table; -Lcom/swmansion/enriched/markdown/utils/common/RenderedSegment$Text; -Lcom/swmansion/enriched/markdown/utils/common/SegmentReconciler; -Lcom/swmansion/enriched/markdown/utils/common/SegmentSignature; +Lcom/swmansion/enriched/markdown/segments/RenderedSegment; +Lcom/swmansion/enriched/markdown/segments/RenderedSegment$Math; +Lcom/swmansion/enriched/markdown/segments/RenderedSegment$Table; +Lcom/swmansion/enriched/markdown/segments/RenderedSegment$Text; +Lcom/swmansion/enriched/markdown/segments/SegmentReconciler; +Lcom/swmansion/enriched/markdown/segments/SegmentSignature; Lcom/swmansion/enriched/markdown/utils/common/StreamingMarkdownFilter; Lcom/swmansion/enriched/markdown/utils/common/TableStreamingMode; Lcom/swmansion/enriched/markdown/utils/common/layout/RTLUtilsKt; @@ -309,19 +308,19 @@ Lcom/swmansion/enriched/markdown/utils/text/view/TextSelectionColorsKt; Lcom/swmansion/enriched/markdown/utils/text/view/TextViewSetupKt; # Views -Lcom/swmansion/enriched/markdown/views/BlockSegmentView; +Lcom/swmansion/enriched/markdown/segments/BlockSegmentView; Lcom/swmansion/enriched/markdown/views/ContextMenuPopup; Lcom/swmansion/enriched/markdown/views/ContextMenuPopup$Builder; Lcom/swmansion/enriched/markdown/views/ContextMenuPopup$Icon; Lcom/swmansion/enriched/markdown/views/ContextMenuPopup$MenuItem; -Lcom/swmansion/enriched/markdown/views/MathContainerView; -Lcom/swmansion/enriched/markdown/views/TableContainerView; -Lcom/swmansion/enriched/markdown/views/TableContainerView$CellBackgroundView; -Lcom/swmansion/enriched/markdown/views/TableContainerView$CellTextView; -Lcom/swmansion/enriched/markdown/views/TableContainerView$GridContainerView; -Lcom/swmansion/enriched/markdown/views/TableContainerView$HeaderTypefaceSpan; -Lcom/swmansion/enriched/markdown/views/TableContainerView$TableCellData; +Lcom/swmansion/enriched/markdown/segments/MathContainerView; +Lcom/swmansion/enriched/markdown/segments/TableContainerView; +Lcom/swmansion/enriched/markdown/segments/TableContainerView$CellBackgroundView; +Lcom/swmansion/enriched/markdown/segments/TableContainerView$CellTextView; +Lcom/swmansion/enriched/markdown/segments/TableContainerView$GridContainerView; +Lcom/swmansion/enriched/markdown/segments/TableContainerView$HeaderTypefaceSpan; +Lcom/swmansion/enriched/markdown/segments/TableContainerView$TableCellData; # Math (optional, from src/math source set) Lcom/swmansion/enriched/markdown/spans/MathMeasureHelper; -Lcom/swmansion/enriched/markdown/views/MathContainerView$RaTeXCanvasView; +Lcom/swmansion/enriched/markdown/segments/MathContainerView$RaTeXCanvasView; diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt index 9bacaa728..a74f663a2 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt @@ -13,26 +13,26 @@ import com.facebook.react.uimanager.StateWrapper import com.swmansion.enriched.markdown.accessibility.AccessibilityLabels import com.swmansion.enriched.markdown.parser.Md4cFlags import com.swmansion.enriched.markdown.parser.Parser +import com.swmansion.enriched.markdown.segments.BlockquoteContainerView +import com.swmansion.enriched.markdown.segments.CodeBlockContainerView +import com.swmansion.enriched.markdown.segments.ContainerNodeView +import com.swmansion.enriched.markdown.segments.MarkdownSegmentRenderer +import com.swmansion.enriched.markdown.segments.RenderedSegment +import com.swmansion.enriched.markdown.segments.SegmentViewConfig +import com.swmansion.enriched.markdown.segments.SegmentViewCreators +import com.swmansion.enriched.markdown.segments.SegmentViewFactory +import com.swmansion.enriched.markdown.segments.TableContainerView +import com.swmansion.enriched.markdown.segments.splitASTIntoSegments import com.swmansion.enriched.markdown.spoiler.SpoilerOverlay import com.swmansion.enriched.markdown.styles.StyleConfig import com.swmansion.enriched.markdown.utils.common.BreakStrategyUtils import com.swmansion.enriched.markdown.utils.common.CodeBlockStreamingMode -import com.swmansion.enriched.markdown.utils.common.MarkdownSegmentRenderer -import com.swmansion.enriched.markdown.utils.common.RenderedSegment import com.swmansion.enriched.markdown.utils.common.StreamingMarkdownFilter import com.swmansion.enriched.markdown.utils.common.TableStreamingMode import com.swmansion.enriched.markdown.utils.common.isReducedMotionEnabled -import com.swmansion.enriched.markdown.utils.common.splitASTIntoSegments import com.swmansion.enriched.markdown.utils.text.TailFadeInAnimator import com.swmansion.enriched.markdown.utils.text.view.SelectionMenuConfig import com.swmansion.enriched.markdown.utils.text.view.applySelectionColors -import com.swmansion.enriched.markdown.views.BlockquoteContainerView -import com.swmansion.enriched.markdown.views.CodeBlockContainerView -import com.swmansion.enriched.markdown.views.ContainerNodeView -import com.swmansion.enriched.markdown.views.SegmentViewConfig -import com.swmansion.enriched.markdown.views.SegmentViewCreators -import com.swmansion.enriched.markdown.views.SegmentViewFactory -import com.swmansion.enriched.markdown.views.TableContainerView import java.util.EnumSet import java.util.concurrent.ExecutorService import java.util.concurrent.Executors diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt index 92c0ad4a8..0c27211fb 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt @@ -8,6 +8,7 @@ import android.util.AttributeSet import android.view.MotionEvent import com.swmansion.enriched.markdown.accessibility.AccessibilityLabels import com.swmansion.enriched.markdown.accessibility.AccessibleMarkdownTextView +import com.swmansion.enriched.markdown.segments.BlockSegmentView import com.swmansion.enriched.markdown.spoiler.SpoilerCapable import com.swmansion.enriched.markdown.spoiler.SpoilerOverlay import com.swmansion.enriched.markdown.spoiler.SpoilerOverlayDrawer @@ -20,7 +21,6 @@ import com.swmansion.enriched.markdown.utils.text.view.cancelJSTouchForLinkTap import com.swmansion.enriched.markdown.utils.text.view.createSelectionActionModeCallback import com.swmansion.enriched.markdown.utils.text.view.reallowParentInterceptIfLinkReleased import com.swmansion.enriched.markdown.utils.text.view.setupAsMarkdownTextView -import com.swmansion.enriched.markdown.views.BlockSegmentView class EnrichedMarkdownInternalText @JvmOverloads diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt index 0ec4c9118..5d4a65a39 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt @@ -14,6 +14,12 @@ import com.facebook.yoga.YogaMeasureOutput import com.swmansion.enriched.markdown.parser.Md4cFlags import com.swmansion.enriched.markdown.parser.Parser import com.swmansion.enriched.markdown.renderer.Renderer +import com.swmansion.enriched.markdown.segments.BlockquoteContainerView +import com.swmansion.enriched.markdown.segments.CodeBlockContainerView +import com.swmansion.enriched.markdown.segments.MarkdownSegmentRenderer +import com.swmansion.enriched.markdown.segments.RenderedSegment +import com.swmansion.enriched.markdown.segments.TableContainerView +import com.swmansion.enriched.markdown.segments.splitASTIntoSegments import com.swmansion.enriched.markdown.spans.MathMeasureRequest import com.swmansion.enriched.markdown.spans.MathMetrics import com.swmansion.enriched.markdown.spans.MathRenderMode @@ -21,8 +27,6 @@ import com.swmansion.enriched.markdown.styles.StyleConfig import com.swmansion.enriched.markdown.utils.common.BreakStrategyUtils import com.swmansion.enriched.markdown.utils.common.CodeBlockStreamingMode import com.swmansion.enriched.markdown.utils.common.FeatureFlags -import com.swmansion.enriched.markdown.utils.common.MarkdownSegmentRenderer -import com.swmansion.enriched.markdown.utils.common.RenderedSegment import com.swmansion.enriched.markdown.utils.common.StreamingMarkdownFilter import com.swmansion.enriched.markdown.utils.common.TableStreamingMode import com.swmansion.enriched.markdown.utils.common.getArrayOrNull @@ -30,11 +34,7 @@ import com.swmansion.enriched.markdown.utils.common.getBooleanOrDefault import com.swmansion.enriched.markdown.utils.common.getMapOrNull import com.swmansion.enriched.markdown.utils.common.getStringOrDefault import com.swmansion.enriched.markdown.utils.common.parseImageRequestHeaders -import com.swmansion.enriched.markdown.utils.common.splitASTIntoSegments import com.swmansion.enriched.markdown.utils.text.extensions.replaceMathSpansWithPlaceholders -import com.swmansion.enriched.markdown.views.BlockquoteContainerView -import com.swmansion.enriched.markdown.views.CodeBlockContainerView -import com.swmansion.enriched.markdown.views.TableContainerView import java.util.concurrent.ConcurrentHashMap import kotlin.math.ceil diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/BlockSegmentView.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/BlockSegmentView.kt similarity index 82% rename from packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/BlockSegmentView.kt rename to packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/BlockSegmentView.kt index e8e96639f..c7cb2ed3d 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/BlockSegmentView.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/BlockSegmentView.kt @@ -1,4 +1,4 @@ -package com.swmansion.enriched.markdown.views +package com.swmansion.enriched.markdown.segments /** * Interface for block-level segment views that declare their own margins diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/BlockquoteContainerView.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/BlockquoteContainerView.kt similarity index 97% rename from packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/BlockquoteContainerView.kt rename to packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/BlockquoteContainerView.kt index 10290037c..5cab50789 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/BlockquoteContainerView.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/BlockquoteContainerView.kt @@ -1,4 +1,4 @@ -package com.swmansion.enriched.markdown.views +package com.swmansion.enriched.markdown.segments import android.content.Context import android.graphics.Canvas @@ -13,9 +13,6 @@ import com.swmansion.enriched.markdown.parser.MarkdownASTNode import com.swmansion.enriched.markdown.styles.BlockquoteStyle import com.swmansion.enriched.markdown.styles.StyleConfig import com.swmansion.enriched.markdown.utils.common.BreakStrategyUtils -import com.swmansion.enriched.markdown.utils.common.MarkdownSegmentRenderer -import com.swmansion.enriched.markdown.utils.common.RenderedSegment -import com.swmansion.enriched.markdown.utils.common.splitASTIntoSegments import kotlin.math.ceil /** diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/CodeBlockContainerView.kt similarity index 98% rename from packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt rename to packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/CodeBlockContainerView.kt index 380c6be09..1df60a9a4 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/CodeBlockContainerView.kt @@ -1,4 +1,4 @@ -package com.swmansion.enriched.markdown.views +package com.swmansion.enriched.markdown.segments import android.content.ClipData import android.content.ClipboardManager @@ -31,6 +31,8 @@ import com.swmansion.enriched.markdown.utils.common.CodeBlockHighlighter import com.swmansion.enriched.markdown.utils.common.CodeBlockNode import com.swmansion.enriched.markdown.utils.text.extensions.applyCodeBlockTextStyle import com.swmansion.enriched.markdown.utils.text.span.SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE +import com.swmansion.enriched.markdown.views.ContextMenuPopup +import com.swmansion.enriched.markdown.views.CopyGlyph import kotlin.math.ceil /** diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/ContainerNodeView.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/ContainerNodeView.kt similarity index 96% rename from packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/ContainerNodeView.kt rename to packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/ContainerNodeView.kt index 85370f745..a2770175d 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/ContainerNodeView.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/ContainerNodeView.kt @@ -1,10 +1,8 @@ -package com.swmansion.enriched.markdown.views +package com.swmansion.enriched.markdown.segments import android.content.Context import android.view.View import android.widget.FrameLayout -import com.swmansion.enriched.markdown.utils.common.RenderedSegment -import com.swmansion.enriched.markdown.utils.common.SegmentReconciler import kotlin.math.ceil import kotlin.math.max diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownSegment.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/MarkdownSegment.kt similarity index 97% rename from packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownSegment.kt rename to packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/MarkdownSegment.kt index 52a1b05d5..4e8b4d743 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownSegment.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/MarkdownSegment.kt @@ -1,4 +1,4 @@ -package com.swmansion.enriched.markdown.utils.common +package com.swmansion.enriched.markdown.segments import com.swmansion.enriched.markdown.parser.MarkdownASTNode diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/RenderedSegment.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/RenderedSegment.kt similarity index 98% rename from packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/RenderedSegment.kt rename to packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/RenderedSegment.kt index 9604dd04c..2eb304975 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/RenderedSegment.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/RenderedSegment.kt @@ -1,4 +1,4 @@ -package com.swmansion.enriched.markdown.utils.common +package com.swmansion.enriched.markdown.segments import android.content.Context import android.text.SpannableString diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/SegmentHeightMeasurer.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/SegmentHeightMeasurer.kt similarity index 97% rename from packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/SegmentHeightMeasurer.kt rename to packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/SegmentHeightMeasurer.kt index 7ea237463..86c63c72a 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/SegmentHeightMeasurer.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/SegmentHeightMeasurer.kt @@ -1,4 +1,4 @@ -package com.swmansion.enriched.markdown.views +package com.swmansion.enriched.markdown.segments import android.content.Context import android.graphics.Paint @@ -7,7 +7,6 @@ import android.text.Layout import android.text.StaticLayout import android.text.TextPaint import com.swmansion.enriched.markdown.styles.StyleConfig -import com.swmansion.enriched.markdown.utils.common.RenderedSegment import com.swmansion.enriched.markdown.utils.text.extensions.replaceMathSpansWithPlaceholders import kotlin.math.ceil diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/SegmentReconciler.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/SegmentReconciler.kt similarity index 98% rename from packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/SegmentReconciler.kt rename to packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/SegmentReconciler.kt index 69aaf329c..7ee1752fa 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/SegmentReconciler.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/SegmentReconciler.kt @@ -1,4 +1,4 @@ -package com.swmansion.enriched.markdown.utils.common +package com.swmansion.enriched.markdown.segments import android.view.View diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/SegmentSignature.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/SegmentSignature.kt similarity index 97% rename from packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/SegmentSignature.kt rename to packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/SegmentSignature.kt index 76d945e3b..561627c94 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/SegmentSignature.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/SegmentSignature.kt @@ -1,4 +1,4 @@ -package com.swmansion.enriched.markdown.utils.common +package com.swmansion.enriched.markdown.segments import com.swmansion.enriched.markdown.parser.MarkdownASTNode diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/SegmentViewCreators.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/SegmentViewCreators.kt similarity index 97% rename from packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/SegmentViewCreators.kt rename to packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/SegmentViewCreators.kt index 0d0d07102..006d54dd0 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/SegmentViewCreators.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/SegmentViewCreators.kt @@ -1,4 +1,4 @@ -package com.swmansion.enriched.markdown.views +package com.swmansion.enriched.markdown.segments import android.content.Context import android.os.Build @@ -11,7 +11,6 @@ import com.swmansion.enriched.markdown.accessibility.AccessibilityLabels import com.swmansion.enriched.markdown.styles.StyleConfig import com.swmansion.enriched.markdown.utils.common.BreakStrategyUtils import com.swmansion.enriched.markdown.utils.common.FeatureFlags -import com.swmansion.enriched.markdown.utils.common.RenderedSegment import com.swmansion.enriched.markdown.utils.text.view.SelectionMenuConfig import com.swmansion.enriched.markdown.utils.text.view.applySelectionColors @@ -122,7 +121,7 @@ object SegmentViewCreators { fun mathContainerClass(): Class<*>? = try { - Class.forName("com.swmansion.enriched.markdown.views.MathContainerView") + Class.forName("com.swmansion.enriched.markdown.segments.MathContainerView") } catch (_: Exception) { null } diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/SegmentViewFactory.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/SegmentViewFactory.kt similarity index 85% rename from packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/SegmentViewFactory.kt rename to packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/SegmentViewFactory.kt index 75f20f75c..497216543 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/SegmentViewFactory.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/SegmentViewFactory.kt @@ -1,7 +1,6 @@ -package com.swmansion.enriched.markdown.views +package com.swmansion.enriched.markdown.segments import android.view.View -import com.swmansion.enriched.markdown.utils.common.RenderedSegment /** * Per-kind view work seam used by ContainerNodeView to build and reconcile diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/TableContainerView.kt similarity index 99% rename from packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt rename to packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/TableContainerView.kt index 97a35485a..dd8964dcb 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/TableContainerView.kt @@ -1,4 +1,4 @@ -package com.swmansion.enriched.markdown.views +package com.swmansion.enriched.markdown.segments import android.content.ClipData import android.content.ClipboardManager diff --git a/packages/react-native-enriched-markdown/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt b/packages/react-native-enriched-markdown/android/src/math/java/com/swmansion/enriched/markdown/segments/MathContainerView.kt similarity index 97% rename from packages/react-native-enriched-markdown/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt rename to packages/react-native-enriched-markdown/android/src/math/java/com/swmansion/enriched/markdown/segments/MathContainerView.kt index 428104279..b4c2bfdce 100644 --- a/packages/react-native-enriched-markdown/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +++ b/packages/react-native-enriched-markdown/android/src/math/java/com/swmansion/enriched/markdown/segments/MathContainerView.kt @@ -1,4 +1,4 @@ -package com.swmansion.enriched.markdown.views +package com.swmansion.enriched.markdown.segments import android.content.ClipData import android.content.ClipboardManager @@ -17,6 +17,7 @@ import com.swmansion.enriched.markdown.spans.MathMeasureRequest import com.swmansion.enriched.markdown.spans.MathRenderMode import com.swmansion.enriched.markdown.styles.MathStyle import com.swmansion.enriched.markdown.styles.StyleConfig +import com.swmansion.enriched.markdown.views.ContextMenuPopup import io.ratex.RaTeXEngine import io.ratex.RaTeXFontLoader import io.ratex.RaTeXRenderer diff --git a/packages/react-native-enriched-markdown/ios/views/ENRMBlockquoteContainerView.h b/packages/react-native-enriched-markdown/ios/segments/ENRMBlockquoteContainerView.h similarity index 100% rename from packages/react-native-enriched-markdown/ios/views/ENRMBlockquoteContainerView.h rename to packages/react-native-enriched-markdown/ios/segments/ENRMBlockquoteContainerView.h diff --git a/packages/react-native-enriched-markdown/ios/views/ENRMBlockquoteContainerView.mm b/packages/react-native-enriched-markdown/ios/segments/ENRMBlockquoteContainerView.mm similarity index 100% rename from packages/react-native-enriched-markdown/ios/views/ENRMBlockquoteContainerView.mm rename to packages/react-native-enriched-markdown/ios/segments/ENRMBlockquoteContainerView.mm diff --git a/packages/react-native-enriched-markdown/ios/views/ENRMCodeBlockContainerView.h b/packages/react-native-enriched-markdown/ios/segments/ENRMCodeBlockContainerView.h similarity index 100% rename from packages/react-native-enriched-markdown/ios/views/ENRMCodeBlockContainerView.h rename to packages/react-native-enriched-markdown/ios/segments/ENRMCodeBlockContainerView.h diff --git a/packages/react-native-enriched-markdown/ios/views/ENRMCodeBlockContainerView.m b/packages/react-native-enriched-markdown/ios/segments/ENRMCodeBlockContainerView.m similarity index 100% rename from packages/react-native-enriched-markdown/ios/views/ENRMCodeBlockContainerView.m rename to packages/react-native-enriched-markdown/ios/segments/ENRMCodeBlockContainerView.m diff --git a/packages/react-native-enriched-markdown/ios/views/ENRMContainerNodeView.h b/packages/react-native-enriched-markdown/ios/segments/ENRMContainerNodeView.h similarity index 100% rename from packages/react-native-enriched-markdown/ios/views/ENRMContainerNodeView.h rename to packages/react-native-enriched-markdown/ios/segments/ENRMContainerNodeView.h diff --git a/packages/react-native-enriched-markdown/ios/views/ENRMContainerNodeView.mm b/packages/react-native-enriched-markdown/ios/segments/ENRMContainerNodeView.mm similarity index 100% rename from packages/react-native-enriched-markdown/ios/views/ENRMContainerNodeView.mm rename to packages/react-native-enriched-markdown/ios/segments/ENRMContainerNodeView.mm diff --git a/packages/react-native-enriched-markdown/ios/views/ENRMMathContainerView.h b/packages/react-native-enriched-markdown/ios/segments/ENRMMathContainerView.h similarity index 100% rename from packages/react-native-enriched-markdown/ios/views/ENRMMathContainerView.h rename to packages/react-native-enriched-markdown/ios/segments/ENRMMathContainerView.h diff --git a/packages/react-native-enriched-markdown/ios/views/ENRMMathContainerView.m b/packages/react-native-enriched-markdown/ios/segments/ENRMMathContainerView.m similarity index 100% rename from packages/react-native-enriched-markdown/ios/views/ENRMMathContainerView.m rename to packages/react-native-enriched-markdown/ios/segments/ENRMMathContainerView.m diff --git a/packages/react-native-enriched-markdown/ios/utils/ENRMSegmentHeightMeasurer.h b/packages/react-native-enriched-markdown/ios/segments/ENRMSegmentHeightMeasurer.h similarity index 100% rename from packages/react-native-enriched-markdown/ios/utils/ENRMSegmentHeightMeasurer.h rename to packages/react-native-enriched-markdown/ios/segments/ENRMSegmentHeightMeasurer.h diff --git a/packages/react-native-enriched-markdown/ios/utils/ENRMSegmentHeightMeasurer.mm b/packages/react-native-enriched-markdown/ios/segments/ENRMSegmentHeightMeasurer.mm similarity index 100% rename from packages/react-native-enriched-markdown/ios/utils/ENRMSegmentHeightMeasurer.mm rename to packages/react-native-enriched-markdown/ios/segments/ENRMSegmentHeightMeasurer.mm diff --git a/packages/react-native-enriched-markdown/ios/views/ENRMTableGridView.h b/packages/react-native-enriched-markdown/ios/segments/ENRMTableGridView.h similarity index 100% rename from packages/react-native-enriched-markdown/ios/views/ENRMTableGridView.h rename to packages/react-native-enriched-markdown/ios/segments/ENRMTableGridView.h diff --git a/packages/react-native-enriched-markdown/ios/views/ENRMTableGridView.m b/packages/react-native-enriched-markdown/ios/segments/ENRMTableGridView.m similarity index 100% rename from packages/react-native-enriched-markdown/ios/views/ENRMTableGridView.m rename to packages/react-native-enriched-markdown/ios/segments/ENRMTableGridView.m diff --git a/packages/react-native-enriched-markdown/ios/views/ENRMTableIOSGridView.h b/packages/react-native-enriched-markdown/ios/segments/ENRMTableIOSGridView.h similarity index 100% rename from packages/react-native-enriched-markdown/ios/views/ENRMTableIOSGridView.h rename to packages/react-native-enriched-markdown/ios/segments/ENRMTableIOSGridView.h diff --git a/packages/react-native-enriched-markdown/ios/views/ENRMTableIOSGridView.m b/packages/react-native-enriched-markdown/ios/segments/ENRMTableIOSGridView.m similarity index 100% rename from packages/react-native-enriched-markdown/ios/views/ENRMTableIOSGridView.m rename to packages/react-native-enriched-markdown/ios/segments/ENRMTableIOSGridView.m diff --git a/packages/react-native-enriched-markdown/ios/utils/RenderedMarkdownSegment.h b/packages/react-native-enriched-markdown/ios/segments/RenderedMarkdownSegment.h similarity index 100% rename from packages/react-native-enriched-markdown/ios/utils/RenderedMarkdownSegment.h rename to packages/react-native-enriched-markdown/ios/segments/RenderedMarkdownSegment.h diff --git a/packages/react-native-enriched-markdown/ios/utils/RenderedMarkdownSegment.m b/packages/react-native-enriched-markdown/ios/segments/RenderedMarkdownSegment.m similarity index 100% rename from packages/react-native-enriched-markdown/ios/utils/RenderedMarkdownSegment.m rename to packages/react-native-enriched-markdown/ios/segments/RenderedMarkdownSegment.m diff --git a/packages/react-native-enriched-markdown/ios/utils/SegmentReconciler.h b/packages/react-native-enriched-markdown/ios/segments/SegmentReconciler.h similarity index 100% rename from packages/react-native-enriched-markdown/ios/utils/SegmentReconciler.h rename to packages/react-native-enriched-markdown/ios/segments/SegmentReconciler.h diff --git a/packages/react-native-enriched-markdown/ios/utils/SegmentReconciler.m b/packages/react-native-enriched-markdown/ios/segments/SegmentReconciler.m similarity index 100% rename from packages/react-native-enriched-markdown/ios/utils/SegmentReconciler.m rename to packages/react-native-enriched-markdown/ios/segments/SegmentReconciler.m diff --git a/packages/react-native-enriched-markdown/ios/utils/SegmentRenderer.h b/packages/react-native-enriched-markdown/ios/segments/SegmentRenderer.h similarity index 100% rename from packages/react-native-enriched-markdown/ios/utils/SegmentRenderer.h rename to packages/react-native-enriched-markdown/ios/segments/SegmentRenderer.h diff --git a/packages/react-native-enriched-markdown/ios/utils/SegmentRenderer.m b/packages/react-native-enriched-markdown/ios/segments/SegmentRenderer.m similarity index 100% rename from packages/react-native-enriched-markdown/ios/utils/SegmentRenderer.m rename to packages/react-native-enriched-markdown/ios/segments/SegmentRenderer.m diff --git a/packages/react-native-enriched-markdown/ios/utils/SegmentViewRegistry.h b/packages/react-native-enriched-markdown/ios/segments/SegmentViewRegistry.h similarity index 100% rename from packages/react-native-enriched-markdown/ios/utils/SegmentViewRegistry.h rename to packages/react-native-enriched-markdown/ios/segments/SegmentViewRegistry.h diff --git a/packages/react-native-enriched-markdown/ios/utils/SegmentViewRegistry.m b/packages/react-native-enriched-markdown/ios/segments/SegmentViewRegistry.m similarity index 100% rename from packages/react-native-enriched-markdown/ios/utils/SegmentViewRegistry.m rename to packages/react-native-enriched-markdown/ios/segments/SegmentViewRegistry.m diff --git a/packages/react-native-enriched-markdown/ios/views/TableContainerView.h b/packages/react-native-enriched-markdown/ios/segments/TableContainerView.h similarity index 100% rename from packages/react-native-enriched-markdown/ios/views/TableContainerView.h rename to packages/react-native-enriched-markdown/ios/segments/TableContainerView.h diff --git a/packages/react-native-enriched-markdown/ios/views/TableContainerView.m b/packages/react-native-enriched-markdown/ios/segments/TableContainerView.m similarity index 100% rename from packages/react-native-enriched-markdown/ios/views/TableContainerView.m rename to packages/react-native-enriched-markdown/ios/segments/TableContainerView.m From 0ea008079ca0c2c791e020d35cd93f269ac27dfe Mon Sep 17 00:00:00 2001 From: Ernest Date: Wed, 19 Aug 2026 09:02:08 +0200 Subject: [PATCH 08/11] fix: copilot comment --- .../enriched/markdown/segments/SegmentViewCreators.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/SegmentViewCreators.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/SegmentViewCreators.kt index 006d54dd0..20750afd1 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/SegmentViewCreators.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/SegmentViewCreators.kt @@ -119,14 +119,20 @@ object SegmentViewCreators { applyCodeBlockNode(segment.node) } - fun mathContainerClass(): Class<*>? = + // Availability is fixed at build time (the math source set is compiled in or not), so the + // reflective lookup - including the ClassNotFoundException when math is disabled - is resolved + // once instead of on every reconcile via isMathContainerView/matchesKind. + private val cachedMathContainerClass: Class<*>? by lazy { try { Class.forName("com.swmansion.enriched.markdown.segments.MathContainerView") } catch (_: Exception) { null } + } + + fun mathContainerClass(): Class<*>? = cachedMathContainerClass - fun isMathContainerView(view: View): Boolean = mathContainerClass()?.isInstance(view) == true + fun isMathContainerView(view: View): Boolean = cachedMathContainerClass?.isInstance(view) == true fun createMathView( segment: RenderedSegment.Math, From f11a98dc847a55402888d7f25f64facab8cc2f1b Mon Sep 17 00:00:00 2001 From: Ernest Date: Wed, 19 Aug 2026 10:16:58 +0200 Subject: [PATCH 09/11] fix: indentation fix for blockquote + list combo --- .../block/Blockquote.stories.tsx | 28 +++++++++++++++---- .../ios/renderer/AttributedRenderer.h | 4 +++ .../ios/renderer/ENRMBlockquoteTextRenderer.m | 23 +++++++++++---- .../ios/utils/ENRMTextRenderer.m | 1 + 4 files changed, 46 insertions(+), 10 deletions(-) diff --git a/apps/react-native-example/.rnstorybook/stories/components/EnrichedMarkdownText/block/Blockquote.stories.tsx b/apps/react-native-example/.rnstorybook/stories/components/EnrichedMarkdownText/block/Blockquote.stories.tsx index 2b79094c8..962312770 100644 --- a/apps/react-native-example/.rnstorybook/stories/components/EnrichedMarkdownText/block/Blockquote.stories.tsx +++ b/apps/react-native-example/.rnstorybook/stories/components/EnrichedMarkdownText/block/Blockquote.stories.tsx @@ -23,12 +23,30 @@ const NESTED_MARKDOWN = `> top-level blockquote >> nested blockquote inside the first >>> deeply nested blockquote`; -const CODE_BLOCK_MARKDOWN = `> Blockquote with a fenced code block inside: +const BLOCK_ELEMENTS_MARKDOWN = `> Blockquote containing several block elements: +> +> ## A heading inside the quote +> +> A paragraph with **bold**, _italic_ and a [link](https://swmansion.com). +> +> - first bullet +> - second bullet +> +> 1. ordered one +> 2. ordered two > > \`\`\`ts > const answer = 42; > \`\`\` > +> | Column A | Column B | +> | -------- | -------- | +> | 1 | 2 | +> +> $$E = mc^2$$ +> +> > a nested blockquote inside +> > and a trailing paragraph.`; const argTypes = { @@ -143,16 +161,16 @@ export const Nested: TextStory = { ), }; -export const WithCodeBlock: TextStory = { +export const WithBlockElements: TextStory = { ...blockquoteStoryBase, args: { ...blockquoteStoryBase.args, - markdown: CODE_BLOCK_MARKDOWN, + markdown: BLOCK_ELEMENTS_MARKDOWN, }, render: (args) => renderBlockquote( - 'Blockquote with Code Block', - 'A fenced code block inside a blockquote. With flavor="github" the code block becomes a real code-block container nested in the quote; with flavor="commonmark" it renders inline. Use the controls to tune markdownStyle.blockquote.', + 'Blockquote with Block Elements', + 'A blockquote holding many block elements - heading, lists, a fenced code block, a table, display math and a nested quote. With flavor="github" each becomes its own segment view (code block, table, math and nested quote as real containers) nested in the quote; with flavor="commonmark" they render inline. Use the controls to tune markdownStyle.blockquote.', args ), }; diff --git a/packages/react-native-enriched-markdown/ios/renderer/AttributedRenderer.h b/packages/react-native-enriched-markdown/ios/renderer/AttributedRenderer.h index 0b0bdbbbf..246387ea2 100644 --- a/packages/react-native-enriched-markdown/ios/renderer/AttributedRenderer.h +++ b/packages/react-native-enriched-markdown/ios/renderer/AttributedRenderer.h @@ -4,6 +4,8 @@ @class MarkdownASTNode; @class RenderContext; +NS_ASSUME_NONNULL_BEGIN + @interface AttributedRenderer : NSObject - (instancetype)initWithConfig:(id)config; // Renders a flat list of sibling nodes into an attributed string: sets the baseline block style, @@ -17,3 +19,5 @@ - (CGFloat)getLastElementMarginBottom; - (void)setAllowTrailingMargin:(BOOL)allow; @end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native-enriched-markdown/ios/renderer/ENRMBlockquoteTextRenderer.m b/packages/react-native-enriched-markdown/ios/renderer/ENRMBlockquoteTextRenderer.m index 0cda58cd4..1626d0d0c 100644 --- a/packages/react-native-enriched-markdown/ios/renderer/ENRMBlockquoteTextRenderer.m +++ b/packages/react-native-enriched-markdown/ios/renderer/ENRMBlockquoteTextRenderer.m @@ -18,16 +18,29 @@ - (instancetype)initWithConfig:(StyleConfig *)config - (void)pushOnContext:(RenderContext *)context { - context.blockquoteDepth = context.blockquoteDepth + 1; + // Only set the blockquote baseline block style (tight paragraphs + the quote's font/color). + // Unlike the commonmark inline path we do NOT bump blockquoteDepth: the ENRMBlockquoteContainerView + // already insets its content by the quote's border/gap/padding, so bumping it here would make + // ListItemRenderer add a second blockquoteIndentForDepth on top and over-indent nested lists. [context setBlockStyle:BlockTypeBlockquote font:_config.blockquoteFont color:_config.blockquoteColor headingLevel:0]; } - (void)postProcess:(NSMutableAttributedString *)text { - // Paragraphs apply their own line height regardless of block type; a quote's own line height is - // stamped over the whole run here, matching Android's BlockquoteTextRenderer. - if (text.length > 0) { - applyLineHeight(text, NSMakeRange(0, text.length), [_config blockquoteLineHeight]); + // Stamp the quote's line height per paragraph. applyLineHeight copies the paragraph style at the + // range start over the whole range, so applying it across the entire run would overwrite every + // paragraph with the first one's style - wiping each list item's firstLineHeadIndent/headIndent + // (the marker draws from a separate attribute, so it would then land on top of un-indented text). + CGFloat lineHeight = [_config blockquoteLineHeight]; + if (text.length == 0 || lineHeight <= 0) { + return; + } + NSString *string = text.string; + NSUInteger location = 0; + while (location < text.length) { + NSRange paragraph = [string paragraphRangeForRange:NSMakeRange(location, 0)]; + applyLineHeight(text, paragraph, lineHeight); + location = NSMaxRange(paragraph); } } diff --git a/packages/react-native-enriched-markdown/ios/utils/ENRMTextRenderer.m b/packages/react-native-enriched-markdown/ios/utils/ENRMTextRenderer.m index b9a31e55e..9ab10cf02 100644 --- a/packages/react-native-enriched-markdown/ios/utils/ENRMTextRenderer.m +++ b/packages/react-native-enriched-markdown/ios/utils/ENRMTextRenderer.m @@ -2,6 +2,7 @@ #import "AccessibilityInfo.h" #import "AttributedRenderer.h" #import "ENRMBlockquoteTextRenderer.h" +#import "ParagraphStyleUtils.h" #import "RenderContext.h" #import "StyleConfig.h" From 516974da1e70108dd5a59c3dcad740f39435c666 Mon Sep 17 00:00:00 2001 From: Ernest Date: Wed, 19 Aug 2026 10:51:07 +0200 Subject: [PATCH 10/11] test: maestro --- .../blockquote_code_block_combo_display.png | Bin 36814 -> 36890 bytes .../android/blockquote_display.png | Bin 24467 -> 24091 bytes .../blockquote_image_combo_display.png | Bin 58211 -> 57871 bytes .../blockquote_inner_list_combo_display.png | Bin 49083 -> 48436 bytes .../android/blockquote_list_combo_display.png | Bin 39654 -> 39411 bytes .../header_blockquote_combo_display.png | Bin 42134 -> 41750 bytes .../android/nested_blockquote_display.png | Bin 33785 -> 34293 bytes .../paragraph_blockquote_combo_display.png | Bin 34238 -> 34240 bytes .../blockquote_code_block_combo_display.png | Bin 32545 -> 32557 bytes .../screenshots/ios/blockquote_display.png | Bin 24245 -> 24278 bytes .../ios/blockquote_image_combo_display.png | Bin 65972 -> 66079 bytes .../blockquote_inner_list_combo_display.png | Bin 42555 -> 0 bytes .../ios/blockquote_list_combo_display.png | Bin 40418 -> 40349 bytes .../ios/header_blockquote_combo_display.png | Bin 41408 -> 41534 bytes .../ios/nested_blockquote_display.png | Bin 35794 -> 35813 bytes 15 files changed, 0 insertions(+), 0 deletions(-) diff --git a/.maestro/enrichedMarkdownText/screenshots/android/blockquote_code_block_combo_display.png b/.maestro/enrichedMarkdownText/screenshots/android/blockquote_code_block_combo_display.png index 68fe46408325fc99cca6f64a8ebaf3006396cac4..e3738a9d750c3500afb2bff1d5aa85af94e4d912 100644 GIT binary patch literal 36890 zcmeFZ2T)U8zb}lUVnY-~RGL0WR}c{p5D*JUmrg)Hg@AM=hLR9KEGQ_5l+Z!x5L#$L zXo^S&X`u(CL+B7nAjw?;pZERF`QCfxo9~`8_uQE~qca)y-fOS5{_Ed=zy0pMmJ0LX zbB7ri7?@S>-O*)WU>pPgIS%dxe^KA6^Jif2VNkuJr0;G%JLrczHCev4v@LL5hR@kS z!1Yte4}RCWXCthZFPT%;2}lVpjfIB@rZ(h>FYCurULHRG>LxQuIYfJ;OmX#6&>IhH zBb>+w*H;zmq!Q2Hu{PGJv)Faf{Cq``%_Z9Rm9nwgTJ5;;F}9Qx8$N>e2ONr~{9uIC zg99_U`GGIY*gvn+Qm9@xEcuGMqZID>M=xu_sfYoH*Q00cFIRi|Ro8h^Y^*2QPRoaL z&^BAWDXY%64qkY5{FdGGBvy6taKt4(NCx(lb$YXIT=Ffyw!YhfMD2KVlFh_`+E?_a zID_qtbX57y69+G71utJ>Gx0Czv=!>I?pPB@!-_iZy?PZkx^U5Ee4>9L^7zi!tPhg3 z)YHT5xW-av$I&F;FSXJmtUS>1z6Jk3Hz1Ns)W3yCIl4MWyEGvB5@(V1wykmKo`n~G zZg5I^sNNiob(DH4QVFF%>Vw{`>vixz``I;ir;`5uUT=5Mhy7 zHhyAval&G{CCToJD=S%gept~ibL+Zhkpi)IyMycY29Hp_>R%NK7J3d2*-Q+v!Xv^=tEq{EjE?D4PgR5Z#{G_c6x(7 z27bP(`Y#^XN$3)y%%hf|*>CwH9foe&)1~h9Xo*-L1Lo|oJLjkB)`s)w4{~_NP9=?~ zl0}dDpos{(ixi!3LQH-BX@m7l>>2Lqis;PA1o4S4#~J7w zQx;BNcIOXlg=>eFLCLLUctG~Qy>=h}@-C#f4>sI`?TQW+(2kE}RHN_Mup|`hM@kBs zw5M8tTjtsPRQ<}htQXn0B<-C2kBvwK&*{^OQcR)rEv>R|02`*EDMm!H#3sOBzrdOg zN`pOp)x5Khu~_b74G8!UClHR*u5cO<*9D1PC>Rh-&#qu@ivn{Nc9Gznc8|wEx`Ahn zOZG(Jv)ZT+a?U>pQj@xIH&CwAGg)RxmK;K^*#VUWIRweN?9wkz+i3M6ODFRer8UORWX>gRCMVql?a;Kou1 zd$CGo55a&~yg+cfDD^uVlPc4$zE8|F$?!I{8xgVX@$r)=?v}N*z#fMC z)xA4M)$aj5`Q=P6dzP^n$$q>qixlPBF5O~L`tw0bXk3_m(KXwCb(W4O_!LLsBfrYl zRCP7`5bjG^>P|8qYkq;{Z=wS1`W=7e=|L@Cg&D+3dcTGb7MvKhd1KPM#mwa_TB}Sh z<#rB-+jd#wA_%$OpN z@T?84BG>hN#fUB#qYXv;va4uSUXhxb)>&zI+~{xnAN+;-y@5s?J{g%3Ub8kN3%EsL#NfQX0r(sqYB!^qMv6N*)26N z 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 literal 36814 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 VQnt_1TR2)MQ3*P; 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 diff --git a/.maestro/enrichedMarkdownText/screenshots/android/blockquote_display.png b/.maestro/enrichedMarkdownText/screenshots/android/blockquote_display.png index 78d36ec6ac439fe103d24ba5376a59ed4fa163e3..d34ec4ce56ac76e45c303a2e27a293fe36bfe1b0 100644 GIT binary patch literal 24091 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 literal 24467 zcmeFZc{tSl`~R(6QrTLrvV}^ekR?f$p%Ph&EW;pVO9+=`EHegWEnCShlqJlJeakSo z5TPi{jBQNHI%CN)hOzx#bX}kC_jlai`#A1D?&G+Bf86trIo>n#o_Q_j`8v<%`8Ykl zuXl&*u)tv!78WjTt=onyEUe=!Ec@IJ?gv*QqBOp-uvnXG-@a+=XElqDK%QvNoSNIQ zf7G(vpzP~7m61x$DE0E4)EhNZv|@ku_rupYWgQQ%w52@u>{gsQb+=nF^4yK!{rmqs z`Zi>4aih=4KpYAu&4^|s#eQG*SM9M2z~RvPa5#fesZ@Ugxo$Lvjl@d^t^Uw9aMByi z!PY6JD$HATe#ZIHPo4#jAA*?Mwujn|#hpK=Yn;?1SvJ_+dj{3D{i*1x$phv}H@+UD zFh7FNGX3?Tpc4B)b#r~ndV9+h4230FLEw4ovFP){x>`vEl26TSD?%Ngf34D*(+;Ii zohYk+CT3bp34FZRK>U4;3tVli6LH`s{x3g2);SnUj`0+Kiy_Y*gznSl^ZhfMR6ufYdR?8zo z>F!1u9F?=!do_6I>^=+I>QGnZr?dm%bl$RLGheSIVbyCA2Xvk$yfci+nr zDg69L+wv()3qoUdBJZ%U@4x{h55DxKX-L2|LC6=hCF;?$zJ}xA)h!ddgwd zN+K=mQeBhw1|Q7Suw7p^)%V06runn!3!gZd&bIfJZv$V!q{ix%u%@k6R<(h{lE3GQ z>uIRhLMOem)Cw%-%~8>cy>Dc>`p2>E^Vz0hbY;d;W|Eph)b2O3+)zI~_TVhS0ag-; zuSfihRoojQivaiY>0|vAaj=Qzw^Y2&?!EWwsbk;H7c|my3o5^Ryiz>6ck!Qhc6K0* z%}w0&_9iYkT>UKW4|+}c{G@TKRoe02zjo|pq}0u=qR)c9LEG!R|NA}Rm*i-aFWP%| z{Q6SscJkY8;zvx)=S#n@eAT)yhGpG-o+}y(-~AhWHLR3-_sPrmM~Hj(@BOn`Y4^g7 zv|BLt-6vW`2Wh*1S)2~r?2YR5y)*yZsrP+Kp|A>~Qz|%eAoA)zANDg&S@aO>`F7lKi0u~mE`MpU1q6}o z*dsApXdrR#ky^nMeEk{o_KVmD`HM%P+p}R1`Fkq|Mq_YX8}$)!quaSfHC%zF5w0?> zMT=}=p9>;W*_wyFzF(|rM69aZV-IgI+VUCku46fCE82Uv#^4r=p#Zf(eOsbio%g(o zGT$DNI#L*A`sA1S4x*`M=FOZ;JyWS@=jaJT)Bp+b{V1_T7k1}1*71wC;CXjj7>^g( zUZ)w?n&6vWZA3o>@gPba&*)_$zYJlBaTzL($^Kjkk+1)9egE9%K&FLR{(?e@RX$xM z`<653hB=Q)Gs5o>EoKO}z<;D{dw2fopWC2l#UfLZ4<^<(HNq@UCuNsP!GrqCqtH{I z*SmaDr>~l;A;(@=PRh%C9=dzn!C*=X-Iv{YS0soi|Apj)J|JuZsSl-Vx=!d}-H-=v zV}t(rr8|$t|A2qKC=v9CH!|-cHfn=g*u59Gf&8L$Hv|7++kJB=XgZX{Oc;!^CV5@6 zAwEh=iPs>IW};qPwaj-OAUXUVyL&%3(dYb9*mIEptOi)(rPu@5m@R)IWvRv*Ck>fG`f-+T6`BK z2F|yn%nns17K-&ecu?a{+RSq?EglxWzOex?Ya$Pq=D>fkmf4{>p}-x6pjh_w6=;Mzij815{|r~A%AR!%eCDQNeU+XTvp zRq4H!uVNkOQ^4w~U%7V8b-2#&V|XVwrFQ3$Nj0Rz#?Z#{@EgS-hrU&OlnItWoLTQ8 z*bZ%67tHA?4W<~hJx6bj<#fhe{d7nFg>gr3{)EpyvON+?k-NI}qsx4j|E$oj z$dvnF9XryG5gUbn6%7d<@TYY{prhQha*`@Q^&knl1>tSy$pI#>!Yi> ziQdtwJJa%3gATRMZ;OnF7M{HE%w1CTaYRh!tn%XtK9Q^Ej9sgQ`Ix1?mE}8MpA$2f z19Tof)%jI2`&bL&GSSHozi*_hUBPF9zvRGQb9yg^ZMMFa^||6!DlBG(TnaXG%Ute< zbJCXjT>kE@x@YcNk^Y$YK4Mcvi%%Oe;oJ3VA%B`@+BooA-a?&u&f7?n4@hEZPG&9gEt3Ufq)x$9IW69gF zM!yM!(Of8|s*hp2snZsfE{PnKQI(RMSM!dx%pV(1?SROg z8>}i!;IV+uI`9%R8ogs=lf5k)&rmS?1*_-FT#_ahtQa#+?VP~dRqr9E%!i5 zxhD&cpo{6_>;9A3=KUoRbc3JyJ?5po#b&-ON#vVFp@(SkwrZ}7M9U&KOf0^Hg0t(v zNYEz8?+H8`40_!mIo;0Bp=mN$UKOd?w_6wE-?;J!^Dkd1A!i%nyxTKFmb{Fi$DClv zR<7ShryN6ySEyC#liVtwGyhh=-ckB~RJI-*z3~d?u|1ach&6_h3O&Po$L44Ml%hX0 zC@7AgQf%MK!!oJ{cGs1_&IgLu!2amIUgkYn-!nQQM63`QM-7x$h<_blp0~TuWA-5c zFFE)2)p7dqY|n%*9TKH9Um7>fhh`UIVah8OT2qOww^*=2A5(176_d8aJCP?ddP6;m zQ6Gmc`*7XtwyYN~teUxWY5e%yZ1SdN+I83M>=QHxSD!hEafu^=>DpG`Yf3$}eCKlA zr(ZIMXNP=YbXbHiE1^zfN)r7gTZxmOXi>SL^!Y6W)jc)10~g^t)PnPlYT)awQEyDY z>ac42+{CqxP7?D!5Jvw+^Pa#JtL9E{PKeY{5fkPU9ZuFYv)FC6#G@iC z$KQ9qE|awzy&{V0?BNlW#GO=wMgkZJawLT%lD?eya_>Tqd4c$=TYIbqG5VnVoSb~PiOo{Plv+cC&Q>4 zCtjYjx3&~$lW{SU6u;!kpi2MbPjt*A&X7WM+J%*T%s+aVr6>>bL<=3>zc1(Z^d7;~W5&uFdh?b!qI73X&P}4d zsPVT+?xPOTObHIRrAygw;@1iANo!A2@M#5BrKt z)9r8b6Z3fJx*oZvB82*P8VD$*MYp6gvdZFlSfD^OhT zcqqKkjE&8StY>VMI_Zkpc9SZqTPV6Jc?rqf(8q8KdTK4-<16b_sl0Xh-KilV_U_Xy z*n1xH*-+|+lx0;}v-oKcDe6ESS{GrPwke|_nwb4lQVwWsaAuff0TWi5wg`P-Tr<{! zi-u4HwH>~qMRANR7>JG%&MFyT!LTo%Vl((rbri{LS)xo~Bgk&F#M_ z!201stK)JU!IeeO1~ooVxVo?on?Qw1uWYYC!WOt`$Eze_FWgOEsC(JF6e(*2mDMiu zN!@N~E8JhVDzu6Zr*`8{!7s!6dg^F75*M+$KN74jIUB|WioVcAwG|EIk&Zy`S~@+t zxMO^L2 zY2XZ%3tmuN^4dfSR&7G~-q7(Gxcof0ymngY6usX9_=}(^pYbG3{r#ADR^4=f) zgu_rp)XvYS3AHvCeF0lY+z-xlzW-GyTehg(zAOqGaN*|dt!uOquhN=ZC031jM7xc7 z)@Mm)PP!Q!7f{72ADFAL!`P)__oEDT4up$hX$FO^HQN&=<;HGgdJa>`2bK&+7vDJ8 zs25`Rwn#R3Zht16zey7oWH|11Wq5gV?gzP%X5mY0W|+o# z>UJt4-B%_sWJbcR>6gdqZ0Cf_HOvBhX;ykdnn#agqU#Vmb$E#SD7m`bWJu5ztg ztq6L3vB#<&EglW#xCAYDE7w#gyb6KU0G?cU*7`T4+ZCI~K!Ar`*pVfpCXOd%s%%LAoHiR9%~ z5pz@)X6uvI9hh~UQ+`^df4eHBZ>%%VT!mceTB7RIfu_p-41D%_^F5T}lIOD#Aw+a| zqLpGI68NpQ>3!J?dT>i(&`+Y06L=qe}5BY~)P4!9^e z;b z>~WcgarJd4m6{Qs5oaSRrMj}3iCns!nO68fTGJ~dWromDC2HZlYa%s&EB=d8CP-m0 zu>^gk*P-LTfKgOk6YDXzH$8#zPtPa^_A&p*agLh*OwjW!j*5BKmZL<1(NtRH{G*-% z5JxbT62H2a=%6yt@kWot7%%UW6L`K0Sj&79ysQ7F;jI9=ekS{oI+#+ck93Wx2A`^M z!y?4AO_Yx{?RCGAuhI(N;~qXuQLKBPl-Du$3y9!C9Gw2s#BvUw>IMmg<4uL+{dD%h zQ6SE=RJ{u$iI8AwtinghzZHpXWE=AkO}{IPmLH{(S1r8}3oCs>9^^26Haf~{=nM0$ zY>+uogw#1d%s4fhzjajYlYK?3V{_FdcmwJ4XQIufTeD={wVqvL5ImvMGNSkLFM}2s zj}89!IIUmf82n(I7)P_8v`?hKMt@i0%=Gh$cpY6 zv0VoZvUpC(jusP5w#KxQ?p2+o7ic8fJn1>1iOOeMRdBZ%-JY|!%}H^$g2(vPjJAwy zor1#@5Cnq|Lx{p%+_%vaVw|+aeb#jP>++Leoa+C$j9Gp=`U#@lqG@Z%aJ_C&z*D!1 zYmk$Y7Bu{ahLa$aksM_*FW2>(W%pN0S=IVrGAMQxsC(CFeit63e?0HNJXHUSsY?86 zWa7|Xf~JgK@qfRg=+_b&CD4Wd8%T*K`faJy-=>$}wOS)JE${4kPYEp@$`nQkd#Y*Z zxElF_a`5ZJG=|p_R2@W89Q;HP-jkS~J246(uJbMvIV;v*vbWzXuJpjYZpZ7mD^98LTPy+6B7oKQZsIW?WCE ztVx^=+32L*!qXhlw!2WC%ip#N9FkqTm0vu&%SVJ9uWo=(1tN<>t?E53Gc-?F_);Or zF&(FAkaYoPTuiRNOzjcY{ylO}dqN8i(R%0Bo!AU#(xkuG#?x)h;ZQpMRDB7YK~*VA=jonAqSy%K1IF8L=9o zLipM`!*MH|-kg1m?*Hj!k(!19MEAB~-AN*A4M)vN#Dtcq-d;UnOA39n9-It^QCYqiz=P;eWz zOZFAKf|U>|5?oNwF7Te^&DcOJ>+E33m3<5|J9w>HkKEW$!fdw5{l<5Px+lX85Qahg zN%=n)shzuP%jBeW+V2jJSB;6Ko*A)3a#cC@gNVm0U%c*<(!GJ@s;_)av}+59)e*;y zqAnIaajbPC{Eqy&M*isVXU#b82IpHRdy0vxT@9l3OAFUr^Uc4}_^+$8(j2w~@mtM+(?^J3o0ay!vg()6hFyI`q#MXP`(^u*~i30R*V}Q7*sRcTIk~ zGRGc1mk*`I8*AF%##5vEXFbn(sr}J(ZLqXhqDVg#x>YRV1R2XY=N+UHaO+FmUxT=} zIl7kRv5qNKh4V2k%Q3;%YIw!xF}OEs&;K86|rwvgl@zpH-3#as1P%0uVzX8kr0lZstu z)!7@^UrtR9U*IYPYmHq>B0dJ$@CD4jCmQhbQakyT7^vQxYu1D#-h`VkV@rk0=_2n< zYcl)ut}}HZLi+X?UE(5V#Wmn*Ti>XVe(pb>2A^uc^6CoOmXfzGOyqhtPVW4h!3xqh1YZ-AH{sV*rY{f_ z(eI8sb_*H_*;r~mOY>F}qZFGdENjw@R>kgS7O#xY7Fw7J-0mZcP3>=MmMn*K1`qH12L@hMNc(J4G~sN7zs&`RZ+<^CA_F!P>2lU zpc(M|*O~|Q@WElpK|V8zxn4bg{{Z3YEV*WBbHW83*e-Y;#dVmrv`~x>M-%7x6MJ#! zGlfKYyOVMz)704T{fqPIaFrJ97lI(VCrsM9Vo>TKUnO7p0m5Na-O>rIME&4zB9&^6 z-_URU?Z!^ZzXQpeF&F0@7zJ0sWP?hOT;blyn@2#nu(crAWkL$fI8ey*@Xj@`=ZP(d z@Z~(M-iV9_f$zx#OU!YXN!U^;7LR%v^XnChJS%!gmj(l|$g) zcJNod@ri={qGR+7^x_f5$mX+0u84xdVIPsoWUrq@2)b^>qAXCZScu3WY$_eT@nSh? z4vCJ}gvB*xPZOH4WLqE`;L(Q#@l-QWtQwsn_bJ#^VOz=q-y!vIdd(^_<=8*^C`m-u z)O>R1$t-Z71Cs=6IK_2n+vDAiV71+1-H7p)u8CVE$92xbG}~cQ9;a7CA+y`&rt;?q zxWN7L)663?v23~+ih0(Py)7j&u4=6BzL^WH8QE<2;V=5@?{0iVK(c+z>is#9N0KY5 z351~{#{Gqs&ObzcIoK^~KqH3V0@KR1fh1)=9Xm*i%lxwtGn!sAV*s0_ga(uWK?u#O zH)wr_IJ2RR?TaYavo>DC_MN8LfRqKYteIrlwhV$=G_3KR-Q50IP?|A(zrEI%3$KZY z`|OgGpy9$Bo(!1&QDg=U(k{=^M74aM>-#|T|X7G8tuD(`-nA< zjb)>uS10ykLp$gC<0UM$i#W$5D>XBLvf7_pSivi31j&PM@%$YTC-x1uZL)qQCs4+% z`}NK%^~vxe!YhM>ynEs_zu5FQey%U#i=gg7^52KsH9q=SEOl(X30U}Z5_msQk|O1g zb_c!{@fLB4nCdchFX$W&Qk1zE*_Dm9jn#k)!eLpBxhGj$>Z#m$W20U_k}_tq&rm&Nl`n)eR&_KYIyH;6O2q8jte>pW>R`Fni~wnr z=aR61{HGlq$qlSJ{n99&JwWdZNH#t^{&;}!(tfKvf1-`!zJzUJNkFfJ*^F}8kP`#2nFtGVfMq-H_&6$4hnDY#Sj++i+WOO9=JmaB@{fDIr{ytdkjo>q$-r> zvgaOp&L-CV;A_^Aslnn6mW_eeUu9h6P6pjy;k85y&%ZhvRJQ@jJri21KM0J=;+M!c zuSO+{RhuYKGB=6ijMdfJ%7qeYN25F29|Z*|EA{ zUFxKZe9#jD37`+_^+K;yO;}H)b!~%P&&~C+DSBJqk|TTU8)L?m7uKsnZVX3B16Xoa zh9wfZwJ0VUXtmj#NL#Y3kBtv{)0i+=JM|EHV<}I#Jn>4?iY2ktX<1}^vdhzC)n_&@ z7dj}lVo}T^TvVn80xSn@CZ|AA=RmTrquQrFkyLTD?D>(d#RK`0!QLdG7^gMjjuL_H z-{pzU+yQ|Zo;_AK|6>G2cfCEVi?kv^RGBNa8pJ>%3ic=B5$kEA7$AoD%s?$~AP83S znQeNK4UWGk&ovZ4%(tg1fJ#+Qy%nlXUvtwE;hC`Y0cxsfTX=RTghk zcr1KYQS~{)GA@0~%p&D4RWw_&G#$8=JJSrlu{|hx}O*% zi;CJ>rCojryD!uE<>(Xw;scBOh@>xfqxwIZ^DkN@O3~+_?L~7Ww~CX@q>mFLZ&@V*%*f%9rHQz60 zL-Wbx$O#w*zx@xWR8|d*xT8t5q(5+6TqO^^Vb}R^Epd=%2}kFD#7j)pv3>n2bHUh( z#R#5S)!LU{Q!$tJ5uzpxmG_;NE&aTpDa)_xLu-ff;P=ab4YHqe!y<5SEk1>uiP10ynOC2_aPJ1 zT%G&wL|65EUkITtqT{Vle499HeM-iFf1|uGqRF~yN~879-3-rPy#?W9ww}w+ao(>Z z@SeZPun@Agy>Cl+HpapigP1Lq`bNz#rLTq4VViToQKoLc^osa9$LzOlh00lHWn*L1LZ%el>X^Pm;bC3pkq;Lp@F8XSl0}yJ`B?HkpdiLCCl^;4-D=9d-~ppACFKhdpu3MPyo{U`cot2OHIvVg43gYF`0JHjK9FofMv0)5a5t^? z>$hQ9ceTD;bijQuixJbn`OwddU+R*kc`WIzIV&NU;uBKEG2t180Kdu&NwQMk)6LkW3F`CCi8(c|){pcDAVGur5(Su(9LGw2iW+pIEHgMGJN^~ zNK$r7c1xNO@Y}ZozT6cDj0aI~FIy8jdemA3l*nN`|AHWEOfA@*R>ar6dib5&t)x2u z6(NTF%ZxOr9wQzt6aFpCA3eHAe+8%#`rChLk&?Jj_-!ovZ&4Y18A?)8$`a?CGxc{) zKEO=)!v4L_|2OQ3Q*deECAqHEp`s^sqDPK`N8dd#58AB1(HWM5tpB%A&HpQ8&i{Xt zdteEqz6?GZtrtYDAKzn;(p>&e<&ORzp-@i&vmriL2vaLRdBo1WDf}f`It7i$t}c@l zJA1(CcU{23+@!&iY|^9Nt~bn98aRUAi8McYity`8CGd1Kyt2+1?~^}~oLg(2zost% zOz3^%o}J(t+2Rh%sk;4CMEtm0cSCF9u?HZNH2)=&hD(d`TX#_glh$S47`ny7Kd-$Q z%ZLyAj9j~TzAmQ{+VFk&67yM%ZQMH%>johOh-;AX>1_4iG7ZHzjIPl|LME=9x=saP zW8IBhP`3EnWs9~ir!xy*%8}a`^W$q2mu(K;XgRyz#_MYM2Bg?Z z1Syq7C5>&aF!RzO+OgB4AP^#Wt+ble-SyU?Ey%l+n5+3Ly|KKo<@#sD=!)!Elb#6e z1K@M~@|}5Zzy?*x9iYX_=`@xE~Y@&13?%2@~7C<6_0pEo2T>1QsU-sUv zqXK->R{E>s{jufP&D7s?)4Z}@y*6!rL7pZ9%G86)18isEjYNbfMEL3pb@#Lap4nXl z<>`vY*P7$Bh5}|=Vs(r1c_a%Uh6Fzfhm;E2r{u`lru`=7HWs)IcO_gkDuH?0(ifme zxZ#W3c&8ufCy913_I{*27|9hZa5LhiO>B3hQ}#cak+PNztQ|M7_hvypBIO8xmj1{0 zoyO)r2g{Lu;TX}{^nZBM_`O5B5^lx(DC5bJzi*ZD2gFtqvwqWz9zuAwn^wIhZ^TJj z*Ewv*%VUV{${x&H+Ji=QwNxK3%y3mc^<;8Fh6>VS?tKPFQjul7XG@aM#zYWZ10BXS zj4M7BGjFdp03uXxa{YN1lVVD>3uUa1;W-;0KE{K;w0^iJv+=sub|*;cd5d5RP|xk9 zswruzH&=*FQC&0X?Nijm&^^jiR|zBi%-Rob%f;)uq~ZrP(36rIfyI`emq$ZXpmRUP zG<^2uH9RaQ@MY~mKa%Ld+rNG#K~3;)!ah=$?Q5(7!6X9+rq1bxzt)Gsi;%h7pn$h9 zfSkTLukLPGZUZTVr(p|)nlc+~ECu4XmfyP~c}oWF4~{7_D4#p4r{p;s+avSbkkXRk zihqswGcP9!fjcq3D6e+JD;J{<^AofFOf2~OI7Ml5K_EFC9I+!Giaf_9E%5#unITN- z{oxGna@m~dX}e{&)TZ$2=)wzgm7xX9w&)z1vtqxkt;dYfp_9y?Itk-P3lxv=rBss$d7VFt$dbjPM@(Ef$nOQ!M!yW^NVZ{MvP#v5yt& z$#Ge{Y-)+Kg8c|kbhDxDqdr?8+o?k_ZmyL|(Y(LC;JQg$*(0aMP*s=nJz0=LHe0_I z`>eg^U21x(NcY#Z?MQDqEB007B^t@Z11T$><#ife zDX-_Mli9k60$v>g)5>c|=) z0DSSh^;q+Si%4}r@$K?&pURBR+X7<9dD{2&O4TMn|4fUandkI~ex{sI`nlP|8{dV- z0i1D+dv+wiYy1Hyj5I`vOWJt5QJuS(8Y4A_LEij^STU;&0Ri+0Z;+mJdu?QfiKvkE_YUpc*5i?eU4gy$Jp{ z-f7E4tQV{xI&0L2K_$>?R{W==^y-iI#dT+bdL-(wS{3kF(R-TQc`QJm!anYzPil>b zAgJ-=q*|~3p-)mBs<&%-2rZiWCPw+6&7VkqCLI7%FqF<6C(Nd|=Xa2&XBZwg(@2YZ#_N`IinDnQi-!*$d)1l8gFE*b}9cyk{$M z@3l@&R>P~2Ekv@j&!G}#Et?b12+q?wFacAxXy5;YU1i4puwirU-{m9e1ayFDMG zg8#fVvI!I*^!_iyE5GpdU-0c(gjFe7)1?Aa&G_l?%XKl;0H|Y{MNnTi{0D=wd%uf8 z>7%4bs)5XU@z+Op`d-Rjcc-8pzA1~9!Vcrnl#9or80dye+9j%&RvSGfKYvqv#I?qz zYozl1Xv=@1i|7#iEF?;|Yu45?+5q{-pzkme+Wrpr-D6Do9=coM)0v&x6Yud?z6`DS zRkb@Yraj(Sw!IU@>V9!nxLh=BFe;JrEV**N;oaBL+}W80wIZb*@eL13i!=7hAK3f4 zA8csC%Czg;cun^3^}{~w+cI|V&_TcyR`<95AO%k62fXir3lW~=N%W_x%)H3{UJRl^ z^o0OvY3;|KN>$w#ouPMQ|MuCp`2CxavtM-T)%Fx|pE=5-JTs zQ^ZD$1jY!iMZ31@1zPc=I1+tg_Qyhk3#_HD`Yt-xZNpTiryaFXEzA)OjBzSB!UouE z7vX5}S6Q$Ib+W0Qg%9_4w1bwFgWoh24!3s%U3q!b2w-K z*S>F*XNvOuXjOu?9H~^)m9Tx33j2`bg`Esec33V#6M?nl{tnM~=9{h$D0h2zdT5-e z3#7%$62P$mQurbA^60S#-zdk#BCrxP=XjJ;xuN^kk3^c!)6;V9BT0PYia|3& zZ-pjeuyyF|MWX<{yGq>nGs=Gf=?8rvhjp1Fv!KteNNJT@fU|7vkR4olXyc3WIeMK2 z?g!(tlE0;_yY_ca9n*pmIX9i&p!VIGTpU2Xr6csLxenvhufCtqGMoKjxLhrB=xi^K z)xHj{QFQHYSa^7@j_06x_- z$AHvr1?8O=n}xzRB395S>baBi(@!}4+Pxk*16}!FW@3RGScK(`TECA6sv4E;AU9e( z3X+711D{m5tOoo`TIIX!l2#3MA|j`_XuSx&^K9skd{;fQQ3GD55H8*iiyE4T?kPjL z(yJG$rU;fegb{qq_0VC`t(n8nMsLss@y@21S#7C?HaWL{h%Ldw*VsB92~e@RrnDGk zV<6Jb#wRX2P>zo7bWY7@`Or@h=lm0yt1h`m>;%i$^YiV3KfXZ*?*b_A72GL~B&@d@ z*Q4~46E$%V(bh2vNWN8~IEt_r*dQ$~Z2be_ufUd+euU zbYZM|)v4kB7{r3Y!jX^a?*Y(hZpzDYyC1>Vm4&~rI}nu8qbT0~u4Z1Sz*^I(T~hry zo3q9+Ba~qI-zRRD(UKVb*%Q*gGhK#g@u>^Oa>4I4gA<uZMzsa8HpkHV zBE};y9Jx>-d7L`HI!!WHDV@*igGa!(UY%wObd*SBb}S@N;9{(685ankV8V5qhqlY* zaZFPik1<4)-0s3>Wu5W(xKqmac9f_BDWEIHE6&O(2ppN2%pOx^Y>)YURMuktT!W|D zg@DLajrplG#{LNj=lL0Wx^(L{%JqXC?a_Fy0A1`)dM={(h-4mK9KvdC}?8@;e^ z)MGkk-S^=!>zkHe*LP{DzHq}ETU<2&$3LFr%h!JbR7L3Nqn=BL&z)l{w07UkO6$ zPMIBJ@D>7A&FZfUc;}wS7dhSvpTC56<~``;O{8c& zwFkQS>9~S|RoTNq9H{R6fiIWJ1(Nb(l#RGKBSEKnL2*ETqYM7Bf!(kDoq|bC$?2*> zvjeew?h39Ly>q(9eS$6+pM2Cls*Z{{7YKvjwy|u;Ss1oBriA$M562}C)??jS8O47s z32=IM)uLwjJ%jy~R#~W(D28gwNQ<=eb!=Dkf;m{ zmlQ`$fU0Yvt9|jpgFgVsR1l}XUhDECarN@qaOZJvP?*j{gMw`G=zV#uk|DVvD&Dyy zEcDLQ5YO5nZ9qHJ{2qDF{ArWn5EMu^PX9+(F0Y#JI&tbh(AJ9B zf!tE~VrP}b+-By9In2U7)#g?*QgP2h&?pNGLS)7Gh&v&;^i*t&%-J$%{|0A%^R$}3HMDC z#daw{yRs5u;Z&D$)`6`U&fi=Y!BkkyAyZcVaoj8PoaOdA)l-T-X%JJs7^FDooN*Dx zT3SrBUFz-b$gQBiN6t;GMs36yAByXyU;BRa_`j$cw`~<brn%&_&|_mO*z#K!unpqgkVW&%?8npgvXYGsQ)BLTOIr<*K%>CU!w6VMdlC&Ctb3JEkBQy!t%$0T30oXJ#mZ-2s zu3fmVvS z5$vO9xU!G`QIV$+^6%L5~eouSb%NvtYbhK1f=H80EhnY zD$%uPx^VP_xM+JO+9izsXO1^AZmYXT^@T)2?TA2?*;^T3NYJ z^_W5Dz&NQNx+sE2+^P+AM;e5T1WkXSwuN;O2ia6;XRuLJHgb_NUC&2rpm?3XcaKk`8zJ(*`om^7^td z;o)xJ)&zK*{+vhWt_{jdfV|8)U(x?*Nf5eftY#u1l%6z)53t_o>$Rkzt4$SM3d-04Pp$6Q2+cx5mjuLTHVU2UVth9lKE0B z>o8~;G~QYcv6iR0+y~T{@0`4Gm0M3i0ujm;%bPJo?Ri?QiP7bs26Z=HGQA>0thD)}d^JE7nIKq=F#)}Wh~`s5e}n?1rcN7P6&OJcfR)FKatOjM)d+Uu;>Jl%eVaL-te-!GREGY==%vDv5Nxn8d$$PcWEtx`PBw=DK7zO(!-dNvm z{EzKfl3=nf#*=Js=kG~)_{XHOR?>v3z- z(BZk}BFg@;X^XuCL7%6v01@0nCwjVf0MD;j^^(!2`p=YQ0x9V`rWd6yZ2+Rh+Bm&( ze$uZ|Q_t$DQq;&wl=SP)MN z>=pB|XBkMI^~_<53Xce9S_D4_p2vZ$BksmRi{CTGPI+0HJN(iiSbwOd0MSmK9cEMJ zBKU28YHiH*RTiMB;wkmc_vPLDatd12*t=9f-=!JZsomFY zs7(LzXM;J*{D0Upf^?L}a<%^=&O2HBh=}60X;&nO-T+R!)!P94)e^ZEpw7p8O2Oh# z+q0k$zlL(ttUm|k{Zv0m+^@N$Tawhj+wQQtP*VrgqArmb0RAgk@eJ1R~V3e5f{&(8lQ&o<`Y1M;jSE8?}2Wzgp8O9M3( zq}9(l(^l&eybTQ)VnSY(FNo8@k}aLcj93FX`)nth!;O@o&Ry0c6G`ozhskCsBUM$ zEg#GtTh7}A5+BeU_Up6)rQqhasjXSpDk0yGA*AxibYj4+HK2x(#uiZHyHOXNe#O-F zHb?UMgLdhx5uBGb#IV|7BU8U>xfQV4-K4;EZ@|H1xHwc62mlq+Ey!qnXZDQ>^g@d? zHj1@MS7%j7#f|r6T7I`x_8zWg5Mw1Yg_O$4#cGxu*t&Td$S)>dO}bUx`>fY}Q*sZe z*lO*moVGCQY_f!0VCP70c_$;SzMw#5yxp*Zl$Etnw5+E65 zP;PX7`9bo={2xMl?LKaOh0a^^sF^?TbRgc*irBXddPyUj`{DC~p%c zIs}+CzU$e{o{UTo_Lus8h*fw0#BJ=-Uc>6ZGnzMb1gZ^M3#1-2Tu^&cFEB0w+Nt*l zEN;%&<4z4arl2i_s&ZemUINhaTWE<)9niH4MIN{e?sfOcL!6n_|k^~_nw?LFEbOV0Xw`}LN zO|+}bV*@fE-;cYC#(iQ<^aeLW=rT|gB;mt#oHARsiO>5c)jpvL+QOr!fQu>QecQ>hvG_~d&PTokv~ZnY zfD|yJ?xra9td#>2sXxo%%8xzH&41Au-bG5VzVgM2ygWApkUT}`QxLhns+XdWXv^SV zJ>+`L3RUeA{c}Su(?E>|Pe7NN_-VZP_GdMv^gC0*5cP@Rg=2(&&inCuoHWpvu)^8` z{++?6cN3tsg{~k?oPU^Q4C4iLO6U6Mdp++utLHNS8Cq5qE+wmNuCY^hYH zV$er76sRsni7f?1=Zh42{8;+SPmR6bcHzkP{8yE&&o;DB_ z-S*4^(j}LOT-BY%qs*Mvy!ae3^iF_uF}YH`>a0tltZd=5XAmd zfi@~4`qe9JPy>((M9+gxnX)zab6b|~t3ZqJ6_N2NJr;Sss_Uf@M$_5_6!$!RBGyE{ zCWr(K?N-5*QSw>~UU*>ut^UBH^QWTyO1Sw{r3S6Iyu4RAPyM3gKLkO^uI90HAsoI4 z>@?ag&aZg11?R^cIX{>@$dQwv4J5+A@m4eBXy{)4hM?)8j#axwq{wvEWGDvt-=A3W zuXEdSw1|wbb6Ru@hn956MTLNK6(In?Fqd z;|J$dR z38ZM`K#}HdulsHfPiK9g;y6piR7}_aYb5CXB-&*e_;;;B+{xW0gXBfHeTENB?lZn% zRf)^4pB2VKE7$ayReZdu**oOiS3hSuCCwK5IuscVqgMOQXg|-rN&lq4oUV`pJbS4l z=W$TD=2#){C~9Y|hFD*78+Lh9^wnZ#t)duoRvAZ42?8oP4vr&bZ;_r-e!Q+`jffit zErQHeh27a6LVlfoi>OC4d4xnM;;-bY12d0$WUkxdjl>j!8 zefSf%21;t6oxQ|5E4S3_ax}MZrA;C*Zp$Wde{A$JZGQeInUW=KYRc8j|5b9 zn*P`~h8t*OC;+VHrBfH(Rok z`quK6nqgmI_Yvfj49+Jf&3mUTDsUOOFBE#6nifhJD?rBU>G1V0*bgAu?pc(H(#QPj zEOhz+dZIMKrz8qF(4B^syE;W9A4#fp~Z-{^A-{Mt-Ya_&)1^<=6dHeIK5!9Sk~*2w%<-=!4gl zjJkvBmgwV?`Z;W}xavc)AD&yc-19YaQMj+s^}=sc8$fID#Hd6Hx5%dc1D39q zeL0V-|7$WxdoZh?JQ7E!E2c1%p~K+XGmNosyL)^fDA|vVx2AI=;iEt+0FFMi_BhC` zj3+yxop36k)bn2N&c7va(Vk6j4@wtfO0~$o_XGb_mnd-}vKs*QXoT%=@1gud;s%Mw z5~$Q{68EBdZV2#Sq-qYeL$?dPqiB(@r{U0mg4sDR2 z3^s}Xi|l9Yl-hS+SW^G5So`mMt^c@tf_zH}NbP>!{-Ih-7OVQT4B5<4)cd1&;8VnE zzts`%_w@PzN?)DDTTRw6@fBOa6;>X0i>ks-cl7r4+mXc$q}J3#o<95bZGaqORZc#r z&Lp5}CHDntPzTIO`H#*Bbe4FL&T+=Vyp7EXMOY1uH+ z1c@kxT#jOC4-0O7nxjWTZ}V}l(AB>F*T+Knq`Qg>VsppTO7YH6;rs=85BJ)9eOQx~ zm9yfC>Infww73ol5Rk{moHUAXDc0Ed+KRH#NYzQ)644bObN~8EM9OmqHSI|`JjaRI zX9zD^#;5;d1NQdjdz!YDhw~Qq zr*tKeC2@|qOqEuB=qM|jUx%rD!0mi0YAGWc7qpXI?L^)-X1CMk#-N9qyh+542 zorxfb|FwMM%b&RsI*#kX4WKW$r3?eTc&Xjj_LLIJpMC`E$iT0BOxEJ4DC7&;{EmYb zLE$nz7F0CSDz5UQUKSY!4IYAu^z0AaujCSNA{Pgwy=(4CIl6_-Nwx-d&4sL22g$mg zaw;8KayuoyUqM^0E1Tn|P%s!l zh6P_vHO=CXPpjKU{JXGBv@8QHYfnJv8f)8E9e7^?P@3$X7duC9ljLSgjnryeN9prgBu~xXrb0xZ_fy5BJ zJDeEhS`BxZ&%&e01wo2aLEeJj!WH?@S0jDyc^!+O`h>w{pVSLq?#~c!t534(*aozf zA3aso2TUj|Qel}tNdC<-jvUD)8JkHpGp|n5vuvsZ`T{~*vYTuN{9!X|k(^@e;ospz zp7OD?Hp+&6J{A_|2m&=&sW(}#cRn$G$QA%`Mgf%z0C7$`onaHI=3mG}!hOn5a{GEv z@v?@gVt(l6_<9dUwvf&Nr93{8)|DI50U}FSNz}Tnx=Y zm)tbei`ilEB7c+}xdaPV$6#Z1)VfTG+JQsOnFayF&}-dQQMH_Fi3{cUbJa*mL5G;H`GBGSQZm7)@EB!PLfxs+WYoO$=(B1Peo{h+mKcS;4s8xjqU?c{`TmfCju z%hRy7Corw<p)XB#s<+6A{Dgy!o$tdAP;%%B~J~^+EzhvijSj2FW^) zRS-AMp`y7Em&T%b+KdHo+cjJaCE1Rjg;YzvfCRALPXQ^L!Cz_ zL1%530kP?t9%j2ziQcSgqiSGxA@;HHwm%4fKQrE9QdiNfOsk_o3x+3)JKR!hP!eF~ zPwU;QR5ZV0VlZqrh%u~f8u}2=D^=CKLL$|Ej5N0rM5BiD2zVsfqX?T$%!`bnb5cwX z=p6Cgb`MU31clro5ogQoZX|mteQ*B1O{P)>+;kVP6oPrmx}mx^?qhj&Bk#FdrlSYL zOu@55)UbZvMUK*DZ7zpl zk4CnNUE;eG`-)O}S=%e6`snZ2GNs(M!P~NX3|LSRofK)4tuHwxo{PYNplio{-@6;^ z;yzm`$m2w=b@?7cKi0FEHP#7SPKa~gfdhwjBO+*E(!xfe_+dEZl(RYh7w5is8th!! z%Oz5!E;dV1O#&P3K>)wb-I5r)6!ahgdeZ6D{164A1D~m@T^d9v>jge=VLf`P@Y|sl zyFEr!^gEj6V?wUKzV6!%R;groDI;HojXVb}#|DyDRPMjMVym;LsZ1rAPXJeVW1z62 zAOG&U<+`GtZ=pb0&xEVFpV&fq$-BfJ*rNq{$UWMl#y7Us-Isu$cOf{rOn0Z-%<U$)rY@EE7M|lC;8Mhp&>LnZ*5JOP7EzUl3z@_*MwFX)2#p?fEm4a&=0~al-1| zjR#rbrv(p{jd6v5R6$g8sGlp^E%wN$EFk)vMqRE=TH!&w#OF$GVG6pJv+Y^=uP!2M z^H;}H-rsATy!l#%DoCVeQTX?GJw1EXn;2w*dhFu-%`pU6hQ!>Ci^&iN-WH`hAWa4B zvRsO2EsRU6D5k+WQcGpdu;t|s*WI0@3aBrO93rIWWluh{!1u%!yO)EeQrGAaJ!>jm-kU*-mFOt_LTK}J56 zq#r(JWLke^f0BQms7GFX$Z2 zYcnnuZ!L}8w3xj^an&hj-9LvRvI?{qurr_(1O&J8gLpi`I16zMmKO&ekx0Z%0bZ0i zboTOlP?*o(lwjUF(Qw)(c(b=vmUtglIdyGkE0cH^L6;tCc&dAeZ|%6w#w5ox76{Bh z6W=#WIV$H0)DiNR9O^yR`%EeWL*tEI8WzPT0)BP+;x z5x?o8H6NYe%^)II`wPfC6{8&yrVI1QhJCD-r5SkeSQBU!c~ES`&;Jla>Ei~WDE*ENXriiPrbqrpsdkW1s&B2ocG=)_@7g3iv7@ty zN=kKF7_;+uHJfjl z68g5Xugx_yMaZe9OZ=bIPf2ICrtA-l4mnDU1srk}fEo|^T1p7bGVE-lu1duWLlEcI zgM$5zXYNF)OA9lqwwLc&(|B{&slKBCZt4|5fyz9Pti9M1_)y3 z`aDP`$ah4(%DPwYEt<}TXB!6N#BA%D1Datj(-fZn%#UpE()2nu{|ZHn$NqT#+T~_O z`1v|bTG62)r;4`;YsTVxVQPO>yP^ZK^DowmSS) ziP4T{wDUL!k@Y~0_O-gFxXr(G%?gc`;wG5Z-8Hx=bBVDMbHL0x^3=lU^aoi!br)%2ElHov`px6xcf@OvfC-w*%Q&p2;I)OS?T iPDp+)!@?gE_#x<4Qgh8I=UsqBDJzTf<|RL0jrtc=)VNdt diff --git a/.maestro/enrichedMarkdownText/screenshots/android/blockquote_image_combo_display.png b/.maestro/enrichedMarkdownText/screenshots/android/blockquote_image_combo_display.png index 589aa78ca54eb48c83051fceb708f2ec1938ccb9..5227084da3fb75f04ae25d14493996681ba0208c 100644 GIT binary patch literal 57871 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