react-native-jet-markdown 0.2.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/JetMarkdown.podspec +42 -0
  2. package/LICENSE +20 -0
  3. package/README.md +247 -0
  4. package/android/CMakeLists.txt +52 -0
  5. package/android/build.gradle +55 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/cpp/JniBindings.cpp +243 -0
  8. package/android/src/main/java/com/jetmarkdown/JetMarkdownNative.kt +114 -0
  9. package/android/src/main/java/com/jetmarkdown/JetMarkdownPackage.kt +22 -0
  10. package/android/src/main/java/com/jetmarkdown/JetMarkdownView.kt +235 -0
  11. package/android/src/main/java/com/jetmarkdown/JetMarkdownViewManager.kt +75 -0
  12. package/android/src/main/java/com/jetmarkdown/editor/EditorSpans.kt +217 -0
  13. package/android/src/main/java/com/jetmarkdown/editor/JetMarkdownEditorManager.kt +224 -0
  14. package/android/src/main/java/com/jetmarkdown/editor/JetMarkdownEditorView.kt +1736 -0
  15. package/android/src/main/java/com/jetmarkdown/measure/MarkdownMeasurer.kt +54 -0
  16. package/android/src/main/java/com/jetmarkdown/parser/AstDecoder.kt +59 -0
  17. package/android/src/main/java/com/jetmarkdown/parser/MdNode.kt +43 -0
  18. package/android/src/main/java/com/jetmarkdown/render/ContentCache.kt +53 -0
  19. package/android/src/main/java/com/jetmarkdown/render/RenderedContent.kt +318 -0
  20. package/android/src/main/java/com/jetmarkdown/render/SpannableRenderer.kt +658 -0
  21. package/android/src/main/java/com/jetmarkdown/render/spans/ChipSpan.kt +17 -0
  22. package/android/src/main/java/com/jetmarkdown/render/spans/InteractiveSpans.kt +11 -0
  23. package/android/src/main/java/com/jetmarkdown/render/spans/MarkdownLineHeightSpan.kt +59 -0
  24. package/android/src/main/java/com/jetmarkdown/render/spans/RunSpan.kt +40 -0
  25. package/android/src/main/java/com/jetmarkdown/style/Fonts.kt +34 -0
  26. package/android/src/main/java/com/jetmarkdown/style/LayoutStyleSpec.kt +71 -0
  27. package/android/src/main/java/com/jetmarkdown/style/PlatformColorResolver.kt +77 -0
  28. package/android/src/main/java/com/jetmarkdown/style/StyleConfig.kt +80 -0
  29. package/android/src/main/java/com/jetmarkdown/style/TextStyleSpec.kt +61 -0
  30. package/android/src/main/java/com/jetmarkdown/views/BlockStackView.kt +292 -0
  31. package/android/src/main/java/com/jetmarkdown/views/BlockTextView.kt +378 -0
  32. package/android/src/main/java/com/jetmarkdown/views/BoxDrawing.kt +50 -0
  33. package/android/src/main/java/com/jetmarkdown/views/MarkdownHost.kt +12 -0
  34. package/android/src/main/java/com/jetmarkdown/views/MarkdownImageView.kt +113 -0
  35. package/android/src/main/java/com/jetmarkdown/views/NestedHorizontalScrollView.kt +87 -0
  36. package/android/src/main/java/com/jetmarkdown/views/TableBlockView.kt +139 -0
  37. package/cpp/core/Ast.h +85 -0
  38. package/cpp/core/AstJson.cpp +129 -0
  39. package/cpp/core/AstJson.h +12 -0
  40. package/cpp/core/AstSerializer.cpp +40 -0
  41. package/cpp/core/AstSerializer.h +21 -0
  42. package/cpp/core/AstToMarkdown.cpp +549 -0
  43. package/cpp/core/AstToMarkdown.h +16 -0
  44. package/cpp/core/EditorRuns.cpp +640 -0
  45. package/cpp/core/EditorRuns.h +94 -0
  46. package/cpp/core/EditorText.cpp +15 -0
  47. package/cpp/core/EditorText.h +18 -0
  48. package/cpp/core/InlineExtensions.cpp +305 -0
  49. package/cpp/core/InlineExtensions.h +17 -0
  50. package/cpp/core/Parser.cpp +470 -0
  51. package/cpp/core/Parser.h +16 -0
  52. package/cpp/core/Preprocess.cpp +181 -0
  53. package/cpp/core/Preprocess.h +14 -0
  54. package/cpp/md4c/LICENSE +22 -0
  55. package/cpp/md4c/VERSION +1 -0
  56. package/cpp/md4c/md4c.c +6462 -0
  57. package/cpp/md4c/md4c.h +407 -0
  58. package/cpp/react/JetMarkdownEditorShadowNode.cpp +54 -0
  59. package/cpp/react/JetMarkdownEditorShadowNode.h +55 -0
  60. package/cpp/react/JetMarkdownEditorState.h +35 -0
  61. package/cpp/react/JetMarkdownMeasurer.cpp +32 -0
  62. package/cpp/react/JetMarkdownMeasurer.h +41 -0
  63. package/cpp/react/JetMarkdownShadowNode.cpp +100 -0
  64. package/cpp/react/JetMarkdownShadowNode.h +53 -0
  65. package/cpp/react/JetMarkdownState.h +52 -0
  66. package/cpp/react/override/react/renderer/components/JetMarkdownViewSpec/ComponentDescriptors.h +21 -0
  67. package/cpp/tests/parser_tests.cpp +662 -0
  68. package/cpp/tests/run_tests.sh +25 -0
  69. package/ios/JetMarkdownView.h +14 -0
  70. package/ios/JetMarkdownView.mm +468 -0
  71. package/ios/editor/JetMarkdownEditor.h +12 -0
  72. package/ios/editor/JetMarkdownEditor.mm +2002 -0
  73. package/ios/measure/JMDMarkdownMeasurer.h +22 -0
  74. package/ios/measure/JMDMarkdownMeasurer.mm +38 -0
  75. package/ios/render/JMDBlock.h +97 -0
  76. package/ios/render/JMDBlock.m +28 -0
  77. package/ios/render/JMDBlockRenderer.h +17 -0
  78. package/ios/render/JMDBlockRenderer.mm +852 -0
  79. package/ios/render/JMDContentCache.h +17 -0
  80. package/ios/render/JMDContentCache.mm +42 -0
  81. package/ios/render/JMDRenderedContent.h +35 -0
  82. package/ios/render/JMDRenderedContent.mm +274 -0
  83. package/ios/style/JMDFontScale.h +33 -0
  84. package/ios/style/JMDLayoutStyle.h +37 -0
  85. package/ios/style/JMDLayoutStyle.mm +68 -0
  86. package/ios/style/JMDStyleConfig.h +39 -0
  87. package/ios/style/JMDStyleConfig.mm +111 -0
  88. package/ios/style/JMDTextStyle.h +25 -0
  89. package/ios/style/JMDTextStyle.mm +68 -0
  90. package/ios/views/JMDBlockStackView.h +18 -0
  91. package/ios/views/JMDBlockStackView.m +97 -0
  92. package/ios/views/JMDBlockTextView.h +24 -0
  93. package/ios/views/JMDBlockTextView.m +340 -0
  94. package/ios/views/JMDBoxViews.h +33 -0
  95. package/ios/views/JMDBoxViews.m +292 -0
  96. package/ios/views/JMDImageView.h +19 -0
  97. package/ios/views/JMDImageView.m +68 -0
  98. package/ios/views/JMDMarkdownHost.h +16 -0
  99. package/ios/views/JMDTableView.h +16 -0
  100. package/ios/views/JMDTableView.m +172 -0
  101. package/lib/module/JetMarkdownEditor.js +85 -0
  102. package/lib/module/JetMarkdownEditor.js.map +1 -0
  103. package/lib/module/JetMarkdownEditor.native.js +244 -0
  104. package/lib/module/JetMarkdownEditor.native.js.map +1 -0
  105. package/lib/module/JetMarkdownEditorNativeComponent.ts +173 -0
  106. package/lib/module/JetMarkdownView.js +19 -0
  107. package/lib/module/JetMarkdownView.js.map +1 -0
  108. package/lib/module/JetMarkdownView.native.js +44 -0
  109. package/lib/module/JetMarkdownView.native.js.map +1 -0
  110. package/lib/module/JetMarkdownViewNativeComponent.ts +35 -0
  111. package/lib/module/defaultStyles.js +122 -0
  112. package/lib/module/defaultStyles.js.map +1 -0
  113. package/lib/module/index.js +9 -0
  114. package/lib/module/index.js.map +1 -0
  115. package/lib/module/package.json +1 -0
  116. package/lib/module/serializeStyles.js +259 -0
  117. package/lib/module/serializeStyles.js.map +1 -0
  118. package/lib/module/types.js +4 -0
  119. package/lib/module/types.js.map +1 -0
  120. package/lib/module/useJetMarkdownEditor.js +41 -0
  121. package/lib/module/useJetMarkdownEditor.js.map +1 -0
  122. package/lib/typescript/package.json +1 -0
  123. package/lib/typescript/src/JetMarkdownEditor.d.ts +6 -0
  124. package/lib/typescript/src/JetMarkdownEditor.d.ts.map +1 -0
  125. package/lib/typescript/src/JetMarkdownEditor.native.d.ts +6 -0
  126. package/lib/typescript/src/JetMarkdownEditor.native.d.ts.map +1 -0
  127. package/lib/typescript/src/JetMarkdownEditorNativeComponent.d.ts +105 -0
  128. package/lib/typescript/src/JetMarkdownEditorNativeComponent.d.ts.map +1 -0
  129. package/lib/typescript/src/JetMarkdownView.d.ts +3 -0
  130. package/lib/typescript/src/JetMarkdownView.d.ts.map +1 -0
  131. package/lib/typescript/src/JetMarkdownView.native.d.ts +3 -0
  132. package/lib/typescript/src/JetMarkdownView.native.d.ts.map +1 -0
  133. package/lib/typescript/src/JetMarkdownViewNativeComponent.d.ts +28 -0
  134. package/lib/typescript/src/JetMarkdownViewNativeComponent.d.ts.map +1 -0
  135. package/lib/typescript/src/defaultStyles.d.ts +20 -0
  136. package/lib/typescript/src/defaultStyles.d.ts.map +1 -0
  137. package/lib/typescript/src/index.d.ts +7 -0
  138. package/lib/typescript/src/index.d.ts.map +1 -0
  139. package/lib/typescript/src/serializeStyles.d.ts +34 -0
  140. package/lib/typescript/src/serializeStyles.d.ts.map +1 -0
  141. package/lib/typescript/src/types.d.ts +352 -0
  142. package/lib/typescript/src/types.d.ts.map +1 -0
  143. package/lib/typescript/src/useJetMarkdownEditor.d.ts +36 -0
  144. package/lib/typescript/src/useJetMarkdownEditor.d.ts.map +1 -0
  145. package/package.json +150 -0
  146. package/react-native.config.js +11 -0
  147. package/src/JetMarkdownEditor.native.tsx +301 -0
  148. package/src/JetMarkdownEditor.tsx +90 -0
  149. package/src/JetMarkdownEditorNativeComponent.ts +173 -0
  150. package/src/JetMarkdownView.native.tsx +67 -0
  151. package/src/JetMarkdownView.tsx +16 -0
  152. package/src/JetMarkdownViewNativeComponent.ts +35 -0
  153. package/src/defaultStyles.ts +102 -0
  154. package/src/index.tsx +34 -0
  155. package/src/serializeStyles.ts +369 -0
  156. package/src/types.ts +438 -0
  157. package/src/useJetMarkdownEditor.ts +50 -0
@@ -0,0 +1,68 @@
1
+ #import "JMDTextStyle.h"
2
+
3
+ #import <React/RCTConvert.h>
4
+
5
+ @implementation JMDTextStyle
6
+
7
+ + (nullable UIColor *)colorFromJson:(nullable id)value {
8
+ if ([value isKindOfClass:[NSNumber class]]) {
9
+ const uint32_t argb = [value unsignedIntValue];
10
+ return [UIColor colorWithRed:((argb >> 16) & 0xFF) / 255.0
11
+ green:((argb >> 8) & 0xFF) / 255.0
12
+ blue:(argb & 0xFF) / 255.0
13
+ alpha:((argb >> 24) & 0xFF) / 255.0];
14
+ }
15
+ if ([value isKindOfClass:[NSDictionary class]]) {
16
+ // Platform colors: {semantic: [...]} / {dynamic: {light, dark, ...}}.
17
+ // RCTConvert resolves them to (possibly dynamic-provider) UIColors that
18
+ // adapt to trait changes at draw time.
19
+ return [RCTConvert UIColor:value];
20
+ }
21
+ return nil;
22
+ }
23
+
24
+ + (nullable instancetype)fromJson:(nullable NSDictionary *)json {
25
+ if (![json isKindOfClass:[NSDictionary class]]) {
26
+ return nil;
27
+ }
28
+ JMDTextStyle *style = [JMDTextStyle new];
29
+ if (style == nil) {
30
+ return nil;
31
+ }
32
+
33
+ NSNumber *fontSize = [json[@"fontSize"] isKindOfClass:[NSNumber class]] ? json[@"fontSize"] : nil;
34
+ NSString *fontWeightString =
35
+ [json[@"fontWeight"] isKindOfClass:[NSString class]] ? json[@"fontWeight"] : nil;
36
+ NSNumber *fontWeight = nil;
37
+ if (fontWeightString != nil) {
38
+ if ([fontWeightString isEqualToString:@"bold"]) {
39
+ fontWeight = @700;
40
+ } else if ([fontWeightString isEqualToString:@"normal"]) {
41
+ fontWeight = @400;
42
+ } else {
43
+ const NSInteger parsed = fontWeightString.integerValue;
44
+ if (parsed >= 100 && parsed <= 900) {
45
+ fontWeight = @(parsed);
46
+ }
47
+ }
48
+ }
49
+
50
+ style->_fontSize = fontSize;
51
+ style->_fontWeight = fontWeight;
52
+ style->_fontFamily =
53
+ [json[@"fontFamily"] isKindOfClass:[NSString class]] ? json[@"fontFamily"] : nil;
54
+ style->_lineHeight =
55
+ [json[@"lineHeight"] isKindOfClass:[NSNumber class]] ? json[@"lineHeight"] : nil;
56
+ style->_color = [self colorFromJson:json[@"color"]];
57
+ style->_fontVariant =
58
+ [json[@"fontVariant"] isKindOfClass:[NSArray class]] ? json[@"fontVariant"] : nil;
59
+ style->_textDecorationColor = [self colorFromJson:json[@"textDecorationColor"]];
60
+ style->_textDecorationLine =
61
+ [json[@"textDecorationLine"] isKindOfClass:[NSString class]] ? json[@"textDecorationLine"] : nil;
62
+ style->_textDecorationStyle =
63
+ [json[@"textDecorationStyle"] isKindOfClass:[NSString class]] ? json[@"textDecorationStyle"] : nil;
64
+ style->_backgroundColor = [self colorFromJson:json[@"backgroundColor"]];
65
+ return style;
66
+ }
67
+
68
+ @end
@@ -0,0 +1,18 @@
1
+ #import <UIKit/UIKit.h>
2
+
3
+ #import "../render/JMDBlock.h"
4
+
5
+ #import "JMDMarkdownHost.h"
6
+
7
+ NS_ASSUME_NONNULL_BEGIN
8
+
9
+ /// Vertical stack of measured blocks; frames come from the measured tree.
10
+ @interface JMDBlockStackView : UIView
11
+
12
+ @property (nonatomic, weak, nullable) id<JMDMarkdownHost> host;
13
+
14
+ - (void)setBlocks:(NSArray<JMDMeasuredBlock *> *)blocks gap:(CGFloat)gap;
15
+
16
+ @end
17
+
18
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,97 @@
1
+ #import "JMDBlockStackView.h"
2
+
3
+ #import "JMDBlockTextView.h"
4
+ #import "JMDBoxViews.h"
5
+ #import "JMDImageView.h"
6
+ #import "JMDTableView.h"
7
+
8
+ @implementation JMDBlockStackView {
9
+ NSArray<JMDMeasuredBlock *> *_measured;
10
+ CGFloat _gap;
11
+ }
12
+
13
+ - (void)setBlocks:(NSArray<JMDMeasuredBlock *> *)blocks gap:(CGFloat)gap {
14
+ _measured = blocks;
15
+ _gap = gap;
16
+
17
+ for (UIView *subview in [self.subviews copy]) {
18
+ [subview removeFromSuperview];
19
+ }
20
+ for (JMDMeasuredBlock *measured in blocks) {
21
+ [self addSubview:[self createViewFor:measured]];
22
+ }
23
+ [self setNeedsLayout];
24
+ }
25
+
26
+ - (UIView *)createViewFor:(JMDMeasuredBlock *)measured {
27
+ switch (measured.block.kind) {
28
+ case JMDBlockKindText: {
29
+ JMDBlockTextView *view = [[JMDBlockTextView alloc] initWithFrame:CGRectZero];
30
+ view.host = self.host;
31
+ view.spoilerColor = measured.block.spoilerColor;
32
+ view.spoilerRadius = measured.block.spoilerRadius;
33
+ view.spoilerContinuous = measured.block.spoilerContinuous;
34
+ view.attributedText = measured.block.attributedText;
35
+ return view;
36
+ }
37
+ case JMDBlockKindCode: {
38
+ JMDCodeBlockView *view = [[JMDCodeBlockView alloc] initWithFrame:CGRectZero];
39
+ [view bind:measured];
40
+ return view;
41
+ }
42
+ case JMDBlockKindQuote: {
43
+ JMDQuoteView *view = [[JMDQuoteView alloc] initWithFrame:CGRectZero];
44
+ [view bind:measured gap:_gap host:self.host];
45
+ return view;
46
+ }
47
+ case JMDBlockKindList: {
48
+ JMDListBlockView *view = [[JMDListBlockView alloc] initWithFrame:CGRectZero];
49
+ [view bind:measured gap:_gap host:self.host];
50
+ return view;
51
+ }
52
+ case JMDBlockKindDivider: {
53
+ UIView *view = [[UIView alloc] initWithFrame:CGRectZero];
54
+ view.backgroundColor = measured.block.dividerColor;
55
+ return view;
56
+ }
57
+ case JMDBlockKindImage: {
58
+ JMDImageView *view = [[JMDImageView alloc] initWithFrame:CGRectZero];
59
+ view.host = self.host;
60
+ [view bind:measured.block];
61
+ return view;
62
+ }
63
+ case JMDBlockKindTable: {
64
+ JMDTableView *view = [[JMDTableView alloc] initWithFrame:CGRectZero];
65
+ [view bind:measured host:self.host];
66
+ return view;
67
+ }
68
+ }
69
+ return [[UIView alloc] initWithFrame:CGRectZero];
70
+ }
71
+
72
+
73
+ // Never the hit view itself: markdown touches belong to the host component
74
+ // view; only nested scrollers (code blocks, tables) claim touches.
75
+ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
76
+ UIView *hit = [super hitTest:point withEvent:event];
77
+ return hit == self ? nil : hit;
78
+ }
79
+
80
+ - (void)layoutSubviews {
81
+ [super layoutSubviews];
82
+ const CGFloat width = self.bounds.size.width;
83
+ CGFloat y = 0;
84
+ for (NSUInteger i = 0; i < _measured.count && i < self.subviews.count; i++) {
85
+ JMDMeasuredBlock *measured = _measured[i];
86
+ const CGFloat childWidth = measured.block.kind == JMDBlockKindImage
87
+ ? MIN(measured.contentWidth, width)
88
+ : width;
89
+ self.subviews[i].frame = CGRectMake(0, y, childWidth, measured.height);
90
+ y += measured.height;
91
+ if (i + 1 < _measured.count) {
92
+ y += _gap;
93
+ }
94
+ }
95
+ }
96
+
97
+ @end
@@ -0,0 +1,24 @@
1
+ #import <UIKit/UIKit.h>
2
+
3
+ #import "../render/JMDBlock.h"
4
+ #import "JMDMarkdownHost.h"
5
+
6
+ NS_ASSUME_NONNULL_BEGIN
7
+
8
+ /// Draws one block's attributed string (TextKit) plus spoiler covers, and
9
+ /// hit-tests links, mentions, and spoilers by character range.
10
+ @interface JMDBlockTextView : UIView
11
+
12
+ @property (nonatomic, copy, nullable) NSAttributedString *attributedText;
13
+ @property (nonatomic, weak, nullable) id<JMDMarkdownHost> host;
14
+ @property (nonatomic, strong, nullable) UIColor *spoilerColor;
15
+ @property (nonatomic, assign) CGFloat spoilerRadius;
16
+ @property (nonatomic, assign) BOOL spoilerContinuous;
17
+
18
+ /// Attributed-string attributes at a point in this view's coordinates;
19
+ /// nil when the point is not on a glyph. Used by the host for hit testing.
20
+ - (nullable NSDictionary *)attributesAtPoint:(CGPoint)point;
21
+
22
+ @end
23
+
24
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,340 @@
1
+ #import "JMDBlockTextView.h"
2
+
3
+ #import <CoreText/CoreText.h>
4
+
5
+ @implementation JMDBlockTextView {
6
+ NSLayoutManager *_layoutManager;
7
+ NSTextContainer *_textContainer;
8
+ NSTextStorage *_textStorage;
9
+ NSMutableSet<NSNumber *> *_hiddenSpoilers;
10
+ }
11
+
12
+ - (instancetype)initWithFrame:(CGRect)frame {
13
+ if (self = [super initWithFrame:frame]) {
14
+ self.backgroundColor = UIColor.clearColor;
15
+ self.contentMode = UIViewContentModeRedraw;
16
+ }
17
+ return self;
18
+ }
19
+
20
+ // Hit-test transparent: the host component view owns all touch handling,
21
+ // so ancestor scroll views treat markdown content like any React view.
22
+ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
23
+ return nil;
24
+ }
25
+
26
+ - (void)setAttributedText:(NSAttributedString *)attributedText {
27
+ if (![_attributedText isEqualToAttributedString:attributedText]) {
28
+ _attributedText = [attributedText copy];
29
+ _layoutManager = nil;
30
+ // The hiding state lives in the storage, which is rebuilt (with the
31
+ // original colors) alongside the layout manager.
32
+ [_hiddenSpoilers removeAllObjects];
33
+ self.isAccessibilityElement = YES;
34
+ self.accessibilityLabel = attributedText.string;
35
+ self.accessibilityTraits = UIAccessibilityTraitStaticText;
36
+ [self setNeedsDisplay];
37
+ }
38
+ }
39
+
40
+ // Lazy TextKit stack shared by drawing, overlay geometry, and hit-testing
41
+ // (and configured identically to the measurer's) — one engine, so they can
42
+ // never disagree about line positions.
43
+ - (NSLayoutManager *)layoutManagerForBounds {
44
+ if (_layoutManager == nil && _attributedText != nil) {
45
+ _textStorage = [[NSTextStorage alloc] initWithAttributedString:_attributedText];
46
+ _layoutManager = [NSLayoutManager new];
47
+ _textContainer =
48
+ [[NSTextContainer alloc] initWithSize:CGSizeMake(self.bounds.size.width, CGFLOAT_MAX)];
49
+ _textContainer.lineFragmentPadding = 0;
50
+ [_layoutManager addTextContainer:_textContainer];
51
+ [_textStorage addLayoutManager:_layoutManager];
52
+ } else if (_textContainer != nil &&
53
+ _textContainer.size.width != self.bounds.size.width) {
54
+ _textContainer.size = CGSizeMake(self.bounds.size.width, CGFLOAT_MAX);
55
+ }
56
+ return _layoutManager;
57
+ }
58
+
59
+ - (void)drawRect:(CGRect)rect {
60
+ NSLayoutManager *layoutManager = [self layoutManagerForBounds];
61
+ if (layoutManager == nil) {
62
+ return;
63
+ }
64
+ // One engine for everything: the same layout manager that positions the
65
+ // overlays also draws the glyphs. NSStringDrawing typesets separately and
66
+ // disagrees with NSLayoutManager about font leading under a lineHeight
67
+ // cap (fonts with a nonzero line gap drift ~gap pt per line), which
68
+ // misaligned overlays on wrapped lines.
69
+ [self syncSpoilerHiding:layoutManager];
70
+ [self drawRunBackgrounds:layoutManager];
71
+ const NSRange glyphRange =
72
+ [layoutManager glyphRangeForTextContainer:self->_textContainer];
73
+ [layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:CGPointZero];
74
+ [layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:CGPointZero];
75
+ [self drawSpoilerCovers:layoutManager];
76
+ }
77
+
78
+ // Unrevealed spoiler text draws fully transparent — the cover hugs the
79
+ // text, so glyphs would leak around it if drawn. Colors mutate on the
80
+ // shared storage (color-only edits don't reflow); originals restore from
81
+ // the immutable _attributedText on reveal.
82
+ - (void)syncSpoilerHiding:(NSLayoutManager *)layoutManager {
83
+ if (self.host == nil || _attributedText.length == 0 || _textStorage == nil) {
84
+ return;
85
+ }
86
+ if (_hiddenSpoilers == nil) {
87
+ _hiddenSpoilers = [NSMutableSet new];
88
+ }
89
+ [_textStorage beginEditing];
90
+ [_attributedText
91
+ enumerateAttribute:JMDSpoilerIDAttributeName
92
+ inRange:NSMakeRange(0, _attributedText.length)
93
+ options:0
94
+ usingBlock:^(NSNumber *spoilerId, NSRange range, BOOL *stop) {
95
+ if (spoilerId == nil) {
96
+ return;
97
+ }
98
+ const BOOL shouldHide =
99
+ ![self.host isSpoilerRevealed:spoilerId.integerValue];
100
+ const BOOL isHidden =
101
+ [self->_hiddenSpoilers containsObject:spoilerId];
102
+ if (shouldHide == isHidden) {
103
+ return;
104
+ }
105
+ if (shouldHide) {
106
+ [self->_textStorage addAttributes:@{
107
+ NSForegroundColorAttributeName : UIColor.clearColor,
108
+ NSUnderlineColorAttributeName : UIColor.clearColor,
109
+ NSStrikethroughColorAttributeName : UIColor.clearColor,
110
+ }
111
+ range:range];
112
+ [self->_hiddenSpoilers addObject:spoilerId];
113
+ } else {
114
+ [self->_attributedText
115
+ enumerateAttributesInRange:range
116
+ options:0
117
+ usingBlock:^(NSDictionary *attrs,
118
+ NSRange subRange,
119
+ BOOL *stopInner) {
120
+ [self->_textStorage
121
+ setAttributes:attrs
122
+ range:subRange];
123
+ }];
124
+ [self->_hiddenSpoilers removeObject:spoilerId];
125
+ }
126
+ }];
127
+ [_textStorage endEditing];
128
+ }
129
+
130
+ // A chip inside an unrevealed spoiler would peek around the cover.
131
+ - (BOOL)isRangeInsideHiddenSpoiler:(NSRange)range {
132
+ if (self.host == nil) {
133
+ return NO;
134
+ }
135
+ __block BOOL hidden = NO;
136
+ [_attributedText
137
+ enumerateAttribute:JMDSpoilerIDAttributeName
138
+ inRange:range
139
+ options:0
140
+ usingBlock:^(NSNumber *spoilerId, NSRange subRange, BOOL *stop) {
141
+ if (spoilerId != nil &&
142
+ ![self.host isSpoilerRevealed:spoilerId.integerValue]) {
143
+ hidden = YES;
144
+ *stop = YES;
145
+ }
146
+ }];
147
+ return hidden;
148
+ }
149
+
150
+ // Approximates iOS's continuous ("squircle") corner curve for a single
151
+ // rect; falls back to a plain rounded rect when the radius dominates.
152
+ static UIBezierPath *JMDChipPath(CGRect rect, CGFloat radius, BOOL continuous) {
153
+ radius = MIN(radius, MIN(rect.size.width, rect.size.height) / 2);
154
+ if (radius <= 0) {
155
+ return [UIBezierPath bezierPathWithRect:rect];
156
+ }
157
+ if (!continuous) {
158
+ return [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius];
159
+ }
160
+ // The standard smooth-corner approximation: control points extend ~1.528x
161
+ // the radius along the edges. Degrades to circular when the rect is too
162
+ // small to fit the extended corners.
163
+ const CGFloat k = 1.528665;
164
+ const CGFloat ext = radius * k;
165
+ if (rect.size.width < 2 * ext || rect.size.height < 2 * ext) {
166
+ return [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius];
167
+ }
168
+ const CGFloat minX = CGRectGetMinX(rect), maxX = CGRectGetMaxX(rect);
169
+ const CGFloat minY = CGRectGetMinY(rect), maxY = CGRectGetMaxY(rect);
170
+ UIBezierPath *path = [UIBezierPath bezierPath];
171
+ [path moveToPoint:CGPointMake(minX + ext, minY)];
172
+ [path addLineToPoint:CGPointMake(maxX - ext, minY)];
173
+ [path addCurveToPoint:CGPointMake(maxX, minY + ext)
174
+ controlPoint1:CGPointMake(maxX - ext + radius, minY)
175
+ controlPoint2:CGPointMake(maxX, minY + ext - radius)];
176
+ [path addLineToPoint:CGPointMake(maxX, maxY - ext)];
177
+ [path addCurveToPoint:CGPointMake(maxX - ext, maxY)
178
+ controlPoint1:CGPointMake(maxX, maxY - ext + radius)
179
+ controlPoint2:CGPointMake(maxX - ext + radius, maxY)];
180
+ [path addLineToPoint:CGPointMake(minX + ext, maxY)];
181
+ [path addCurveToPoint:CGPointMake(minX, maxY - ext)
182
+ controlPoint1:CGPointMake(minX + ext - radius, maxY)
183
+ controlPoint2:CGPointMake(minX, maxY - ext + radius)];
184
+ [path addLineToPoint:CGPointMake(minX, minY + ext)];
185
+ [path addCurveToPoint:CGPointMake(minX + ext, minY)
186
+ controlPoint1:CGPointMake(minX, minY + ext - radius)
187
+ controlPoint2:CGPointMake(minX + ext - radius, minY)];
188
+ [path closePath];
189
+ return path;
190
+ }
191
+
192
+ // Overlays hug the text. lineHeight moves line boxes around the text, so
193
+ // any box derived from them inherits that skew; these rects anchor on the
194
+ // drawn baseline instead. Horizontal comes from the segment's glyph ink;
195
+ // vertical is the FONT's ink envelope (cap height above the baseline,
196
+ // descender depth below) so every run of a font renders the same height
197
+ // whether or not its particular glyphs have capitals or descenders.
198
+ static const CGFloat JMDInkPad = 2;
199
+
200
+ // Per-line overlay rects for a character range. Whitespace-only segments
201
+ // have no ink and produce no rect.
202
+ - (NSArray<NSValue *> *)inkRectsForRange:(NSRange)range
203
+ layoutManager:(NSLayoutManager *)layoutManager
204
+ padLeft:(CGFloat)padLeft
205
+ padRight:(CGFloat)padRight {
206
+ CGContextRef context = UIGraphicsGetCurrentContext();
207
+ // CTLineGetImageBounds reports bounds relative to the context's current
208
+ // text position, and NSStringDrawing leaves it wherever the last drawn
209
+ // line ended.
210
+ CGContextSetTextPosition(context, 0, 0);
211
+ const CGFloat maxWidth = self.bounds.size.width;
212
+ const CGFloat maxHeight = self.bounds.size.height;
213
+ const NSRange glyphRange =
214
+ [layoutManager glyphRangeForCharacterRange:range actualCharacterRange:nil];
215
+ NSMutableArray<NSValue *> *lineRects = [NSMutableArray new];
216
+ NSUInteger glyphIndex = glyphRange.location;
217
+ while (glyphIndex < NSMaxRange(glyphRange)) {
218
+ NSRange lineGlyphRange;
219
+ const CGRect fragment =
220
+ [layoutManager lineFragmentRectForGlyphAtIndex:glyphIndex
221
+ effectiveRange:&lineGlyphRange];
222
+ const NSRange lineRange = NSIntersectionRange(lineGlyphRange, glyphRange);
223
+ if (lineRange.length == 0) {
224
+ break;
225
+ }
226
+ const NSRange charRange =
227
+ [layoutManager characterRangeForGlyphRange:lineRange actualGlyphRange:nil];
228
+ const CGPoint startLocation =
229
+ [layoutManager locationForGlyphAtIndex:lineRange.location];
230
+ // The typesetter already folds NSBaselineOffset (lineHeight centering,
231
+ // sup/sub shifts) into the glyph location.
232
+ const CGFloat baselineY = fragment.origin.y + startLocation.y;
233
+ const CGFloat penX = fragment.origin.x + startLocation.x;
234
+
235
+ CTLineRef line = CTLineCreateWithAttributedString(
236
+ (__bridge CFAttributedStringRef)[self->_attributedText
237
+ attributedSubstringFromRange:charRange]);
238
+ const CGRect ink = CTLineGetImageBounds(line, context);
239
+ CFRelease(line);
240
+ if (!CGRectIsNull(ink) && ink.size.width > 0) {
241
+ UIFont *font =
242
+ [self->_attributedText attribute:NSFontAttributeName
243
+ atIndex:charRange.location
244
+ effectiveRange:nil]
245
+ ?: [UIFont systemFontOfSize:UIFont.systemFontSize];
246
+ const CGFloat top = MAX(baselineY - font.capHeight - JMDInkPad, 0);
247
+ const CGFloat bottom =
248
+ MIN(baselineY - font.descender + JMDInkPad, maxHeight);
249
+ const CGFloat left = MAX(penX + CGRectGetMinX(ink) - padLeft, 0);
250
+ const CGFloat right = MIN(penX + CGRectGetMaxX(ink) + padRight, maxWidth);
251
+ if (right > left && bottom > top) {
252
+ [lineRects addObject:[NSValue valueWithCGRect:CGRectMake(
253
+ left, top,
254
+ right - left,
255
+ bottom - top)]];
256
+ }
257
+ }
258
+ glyphIndex = NSMaxRange(lineGlyphRange);
259
+ }
260
+ return lineRects;
261
+ }
262
+
263
+ // Run background chips (inlineCode/link/mention and plain highlights),
264
+ // drawn UNDER the text; enumerates the display copy so chips hidden with
265
+ // their spoiler don't draw.
266
+ - (void)drawRunBackgrounds:(NSLayoutManager *)layoutManager {
267
+ if (_attributedText.length == 0) {
268
+ return;
269
+ }
270
+ [_attributedText
271
+ enumerateAttribute:JMDRunBackgroundAttributeName
272
+ inRange:NSMakeRange(0, _attributedText.length)
273
+ options:0
274
+ usingBlock:^(JMDRunBackground *chip, NSRange range, BOOL *stop) {
275
+ if (chip == nil || [self isRangeInsideHiddenSpoiler:range]) {
276
+ return;
277
+ }
278
+ NSArray<NSValue *> *rects = [self
279
+ inkRectsForRange:range
280
+ layoutManager:layoutManager
281
+ padLeft:chip.padLeft > 0 ? chip.padLeft : JMDInkPad
282
+ padRight:chip.padRight > 0 ? chip.padRight
283
+ : JMDInkPad];
284
+ [chip.color setFill];
285
+ for (NSValue *value in rects) {
286
+ [JMDChipPath(value.CGRectValue, chip.radius,
287
+ chip.continuousCurve) fill];
288
+ }
289
+ }];
290
+ }
291
+
292
+ // Spoiler cover chips, drawn OVER the (transparent) text until revealed.
293
+ // Same ink geometry as the run backgrounds so covers and backgrounds look
294
+ // identical.
295
+ - (void)drawSpoilerCovers:(NSLayoutManager *)layoutManager {
296
+ if (_attributedText.length == 0 || self.host == nil) {
297
+ return;
298
+ }
299
+ [_attributedText
300
+ enumerateAttribute:JMDSpoilerIDAttributeName
301
+ inRange:NSMakeRange(0, _attributedText.length)
302
+ options:0
303
+ usingBlock:^(NSNumber *spoilerId, NSRange range, BOOL *stop) {
304
+ if (spoilerId == nil ||
305
+ [self.host isSpoilerRevealed:spoilerId.integerValue]) {
306
+ return;
307
+ }
308
+ NSArray<NSValue *> *rects =
309
+ [self inkRectsForRange:range
310
+ layoutManager:layoutManager
311
+ padLeft:JMDInkPad
312
+ padRight:JMDInkPad];
313
+ [self.spoilerColor ?: UIColor.darkGrayColor setFill];
314
+ for (NSValue *value in rects) {
315
+ [JMDChipPath(value.CGRectValue, self.spoilerRadius,
316
+ self.spoilerContinuous) fill];
317
+ }
318
+ }];
319
+ }
320
+
321
+ - (nullable NSDictionary *)attributesAtPoint:(CGPoint)point {
322
+ NSLayoutManager *layoutManager = [self layoutManagerForBounds];
323
+ if (layoutManager == nil || _attributedText.length == 0) {
324
+ return nil;
325
+ }
326
+ const NSUInteger glyphIndex = [layoutManager glyphIndexForPoint:point
327
+ inTextContainer:_textContainer];
328
+ const CGRect glyphRect = [layoutManager boundingRectForGlyphRange:NSMakeRange(glyphIndex, 1)
329
+ inTextContainer:_textContainer];
330
+ if (!CGRectContainsPoint(CGRectInset(glyphRect, -8, -4), point)) {
331
+ return nil;
332
+ }
333
+ const NSUInteger charIndex = [layoutManager characterIndexForGlyphAtIndex:glyphIndex];
334
+ if (charIndex >= _attributedText.length) {
335
+ return nil;
336
+ }
337
+ return [_attributedText attributesAtIndex:charIndex effectiveRange:nil];
338
+ }
339
+
340
+ @end
@@ -0,0 +1,33 @@
1
+ #import <UIKit/UIKit.h>
2
+
3
+ #import "../render/JMDBlock.h"
4
+ #import "JMDMarkdownHost.h"
5
+
6
+ NS_ASSUME_NONNULL_BEGIN
7
+
8
+ /// Horizontal scroller for code blocks and tables. Cancels React's surface
9
+ /// touch handler when a drag begins so a wrapping Pressable's press dies,
10
+ /// exactly like React Native's own scroll views.
11
+ @interface JMDNestedScrollView : UIScrollView <UIScrollViewDelegate>
12
+ @end
13
+
14
+ /// Block quote: paints its box style, hosts a nested stack inside padding.
15
+ @interface JMDQuoteView : UIView
16
+ - (void)bind:(JMDMeasuredBlock *)measured
17
+ gap:(CGFloat)gap
18
+ host:(nullable id<JMDMarkdownHost>)host;
19
+ @end
20
+
21
+ /// Code block: paints its box, hosts unwrapped text in a horizontal scroller.
22
+ @interface JMDCodeBlockView : UIView
23
+ - (void)bind:(JMDMeasuredBlock *)measured;
24
+ @end
25
+
26
+ /// List: rows of a fixed-width marker column and nested content stacks.
27
+ @interface JMDListBlockView : UIView
28
+ - (void)bind:(JMDMeasuredBlock *)measured
29
+ gap:(CGFloat)gap
30
+ host:(nullable id<JMDMarkdownHost>)host;
31
+ @end
32
+
33
+ NS_ASSUME_NONNULL_END