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,25 @@
1
+ #!/usr/bin/env bash
2
+ # Builds and runs the shared-core golden tests.
3
+ set -euo pipefail
4
+
5
+ cd "$(dirname "$0")"
6
+
7
+ BUILD_DIR="${TMPDIR:-/tmp}/jetmarkdown-tests"
8
+ mkdir -p "$BUILD_DIR"
9
+
10
+ clang -c -O1 -o "$BUILD_DIR/md4c.o" ../md4c/md4c.c
11
+
12
+ clang++ -std=c++17 -O1 -Wall -Wextra \
13
+ -o "$BUILD_DIR/parser_tests" \
14
+ parser_tests.cpp \
15
+ ../core/Parser.cpp \
16
+ ../core/Preprocess.cpp \
17
+ ../core/InlineExtensions.cpp \
18
+ ../core/AstJson.cpp \
19
+ ../core/AstSerializer.cpp \
20
+ ../core/AstToMarkdown.cpp \
21
+ ../core/EditorText.cpp \
22
+ ../core/EditorRuns.cpp \
23
+ "$BUILD_DIR/md4c.o"
24
+
25
+ "$BUILD_DIR/parser_tests"
@@ -0,0 +1,14 @@
1
+ #import <React/RCTViewComponentView.h>
2
+ #import <UIKit/UIKit.h>
3
+
4
+ #ifndef JetMarkdownViewNativeComponent_h
5
+ #define JetMarkdownViewNativeComponent_h
6
+
7
+ NS_ASSUME_NONNULL_BEGIN
8
+
9
+ @interface JetMarkdownView : RCTViewComponentView
10
+ @end
11
+
12
+ NS_ASSUME_NONNULL_END
13
+
14
+ #endif /* JetMarkdownViewNativeComponent_h */
@@ -0,0 +1,468 @@
1
+ #import "JetMarkdownView.h"
2
+
3
+ #import <React/RCTConversions.h>
4
+
5
+ #import <react/renderer/components/JetMarkdownViewSpec/EventEmitters.h>
6
+ #import <react/renderer/components/JetMarkdownViewSpec/Props.h>
7
+ #import <react/renderer/core/ConcreteComponentDescriptor.h>
8
+
9
+ // Imported directly (not via the override ComponentDescriptors.h) so the
10
+ // custom measurable shadow node is used regardless of header search order.
11
+ #import "react/JetMarkdownShadowNode.h"
12
+
13
+ namespace facebook::react {
14
+ using JMDComponentDescriptor = ConcreteComponentDescriptor<JetMarkdownShadowNode>;
15
+ } // namespace facebook::react
16
+
17
+ #import "RCTFabricComponentsPlugins.h"
18
+
19
+ #import "measure/JMDMarkdownMeasurer.h"
20
+ #import "render/JMDContentCache.h"
21
+ #import "style/JMDFontScale.h"
22
+ #import "style/JMDStyleConfig.h"
23
+ #import "views/JMDBlockStackView.h"
24
+ #import "views/JMDBlockTextView.h"
25
+ #import "views/JMDImageView.h"
26
+ #import "views/JMDMarkdownHost.h"
27
+
28
+ #import "react/JetMarkdownMeasurer.h"
29
+
30
+ using namespace facebook::react;
31
+
32
+ @interface JetMarkdownView () <JMDMarkdownHost, UIGestureRecognizerDelegate>
33
+ @end
34
+
35
+ static void JMDSetNeedsDisplayDeep(UIView *view);
36
+ static CGRect JMDScreenFrameOfView(UIView *view);
37
+
38
+ @implementation JetMarkdownView {
39
+ NSString *_markdown;
40
+ NSString *_stylesJson;
41
+ BOOL _allowFontScaling;
42
+ JMDBlockStackView *_stack;
43
+ NSString *_boundKey;
44
+ CGFloat _boundWidth;
45
+ // url -> @[w, h] points: from the images prop (wins) and loaded bitmaps.
46
+ NSMutableDictionary<NSString *, NSArray<NSNumber *> *> *_propImageSizes;
47
+ NSMutableDictionary<NSString *, NSArray<NSNumber *> *> *_loadedImageSizes;
48
+ NSMutableSet<NSNumber *> *_revealedSpoilers;
49
+ JetMarkdownShadowNode::ConcreteState::Shared _state;
50
+ }
51
+
52
+ + (void)load {
53
+ jetmarkdown::JetMarkdownMeasurer::shared().install(
54
+ [](const std::string &markdown,
55
+ const std::string &stylesJson,
56
+ const std::string &imagesJson,
57
+ float maxWidth,
58
+ float fontScale) -> float {
59
+ NSString *markdownString =
60
+ [[NSString alloc] initWithBytes:markdown.data()
61
+ length:markdown.size()
62
+ encoding:NSUTF8StringEncoding];
63
+ NSString *stylesString =
64
+ [[NSString alloc] initWithBytes:stylesJson.data()
65
+ length:stylesJson.size()
66
+ encoding:NSUTF8StringEncoding];
67
+ NSString *imagesString =
68
+ [[NSString alloc] initWithBytes:imagesJson.data()
69
+ length:imagesJson.size()
70
+ encoding:NSUTF8StringEncoding];
71
+ return [JMDMarkdownMeasurer measureMarkdown:markdownString ?: @""
72
+ stylesJson:stylesString ?: @""
73
+ imagesJson:imagesString ?: @""
74
+ maxWidth:maxWidth
75
+ fontScale:fontScale];
76
+ });
77
+ }
78
+
79
+ + (ComponentDescriptorProvider)componentDescriptorProvider {
80
+ return concreteComponentDescriptorProvider<JMDComponentDescriptor>();
81
+ }
82
+
83
+ - (instancetype)initWithFrame:(CGRect)frame {
84
+ if (self = [super initWithFrame:frame]) {
85
+ static const auto defaultProps = std::make_shared<const JetMarkdownViewProps>();
86
+ _props = defaultProps;
87
+ _markdown = @"";
88
+ _stylesJson = @"";
89
+ _allowFontScaling = YES;
90
+
91
+ // Dynamic Type changes re-render in place; RN relayouts the shadow
92
+ // tree with the new multiplier on its own.
93
+ [NSNotificationCenter.defaultCenter
94
+ addObserver:self
95
+ selector:@selector(jmdContentSizeCategoryDidChange)
96
+ name:UIContentSizeCategoryDidChangeNotification
97
+ object:nil];
98
+ _stack = [[JMDBlockStackView alloc] initWithFrame:CGRectZero];
99
+ _propImageSizes = [NSMutableDictionary new];
100
+ _loadedImageSizes = [NSMutableDictionary new];
101
+ _revealedSpoilers = [NSMutableSet new];
102
+ _stack.host = self;
103
+ [self addSubview:_stack];
104
+
105
+ // The internal tree is hit-test transparent; the component view owns
106
+ // link/mention/spoiler/image touch handling (like React Native's Text).
107
+ UITapGestureRecognizer *tap =
108
+ [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(jmdHandleTap:)];
109
+ UILongPressGestureRecognizer *longPress =
110
+ [[UILongPressGestureRecognizer alloc] initWithTarget:self
111
+ action:@selector(jmdHandleLongPress:)];
112
+ tap.delegate = self;
113
+ longPress.delegate = self;
114
+ [self addGestureRecognizer:tap];
115
+ [self addGestureRecognizer:longPress];
116
+ }
117
+ return self;
118
+ }
119
+
120
+ #pragma mark - Interaction resolution
121
+
122
+ // Deepest content view at a host-space point (internal views are hit-test
123
+ // transparent, so this walks frames directly). Views that opt out of user
124
+ // interaction (border overlays) are skipped.
125
+ - (UIView *)jmdContentViewAtPoint:(CGPoint)point inView:(UIView *)view {
126
+ for (UIView *subview in [view.subviews reverseObjectEnumerator]) {
127
+ if (subview.hidden || subview.alpha < 0.01 || !subview.userInteractionEnabled) {
128
+ continue;
129
+ }
130
+ const CGPoint local = [view convertPoint:point toView:subview];
131
+ if ([subview pointInside:local withEvent:nil]) {
132
+ return [self jmdContentViewAtPoint:local inView:subview];
133
+ }
134
+ }
135
+ return view;
136
+ }
137
+
138
+ - (nullable UIView *)jmdInteractiveViewAtPoint:(CGPoint)point
139
+ localPoint:(CGPoint *)localPoint {
140
+ UIView *view = [self jmdContentViewAtPoint:point inView:self];
141
+ while (view != nil && view != self) {
142
+ if ([view isKindOfClass:[JMDBlockTextView class]] ||
143
+ [view isKindOfClass:[JMDImageView class]]) {
144
+ if (localPoint != nil) {
145
+ *localPoint = [self convertPoint:point toView:view];
146
+ }
147
+ return view;
148
+ }
149
+ view = view.superview;
150
+ }
151
+ return nil;
152
+ }
153
+
154
+ - (BOOL)jmdIsInteractiveAtPoint:(CGPoint)point {
155
+ CGPoint local;
156
+ UIView *view = [self jmdInteractiveViewAtPoint:point localPoint:&local];
157
+ if ([view isKindOfClass:[JMDImageView class]]) {
158
+ return ((JMDImageView *)view).imageUrl != nil;
159
+ }
160
+ if ([view isKindOfClass:[JMDBlockTextView class]]) {
161
+ NSDictionary *attributes = [(JMDBlockTextView *)view attributesAtPoint:local];
162
+ return attributes[JMDLinkURLAttributeName] != nil ||
163
+ attributes[JMDSpoilerIDAttributeName] != nil;
164
+ }
165
+ return NO;
166
+ }
167
+
168
+ // Touches that do not start on an interactive range are never tracked, so
169
+ // they cannot interfere with an ancestor scroll view's pan.
170
+ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)recognizer
171
+ shouldReceiveTouch:(UITouch *)touch {
172
+ return [self jmdIsInteractiveAtPoint:[touch locationInView:self]];
173
+ }
174
+
175
+ // Non-interactive points fall through to ancestors so a wrapping pressable
176
+ // becomes the hit view. UIControl-based wrappers (react-native-gesture-handler's
177
+ // button) need the touch delivered to them directly and never fire while this
178
+ // view claims every point. Interactive points stay claimed for the host
179
+ // recognizers; nested code/table scroll views are returned by super unchanged.
180
+ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
181
+ UIView *result = [super hitTest:point withEvent:event];
182
+ if (result == self && ![self jmdIsInteractiveAtPoint:point]) {
183
+ return nil;
184
+ }
185
+ return result;
186
+ }
187
+
188
+ // Scrolling always wins: a drag that begins on a link still pans the list
189
+ // (the tap/long-press simply fail on movement).
190
+ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)recognizer
191
+ shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)other {
192
+ return [other.view isKindOfClass:[UIScrollView class]];
193
+ }
194
+
195
+ - (void)jmdHandleTap:(UITapGestureRecognizer *)recognizer {
196
+ CGPoint local;
197
+ UIView *view = [self jmdInteractiveViewAtPoint:[recognizer locationInView:self]
198
+ localPoint:&local];
199
+ if ([view isKindOfClass:[JMDImageView class]]) {
200
+ NSString *url = ((JMDImageView *)view).imageUrl;
201
+ if (url != nil) {
202
+ [self imagePressed:url frame:JMDScreenFrameOfView(view)];
203
+ }
204
+ return;
205
+ }
206
+ if (![view isKindOfClass:[JMDBlockTextView class]]) {
207
+ return;
208
+ }
209
+ NSDictionary *attributes = [(JMDBlockTextView *)view attributesAtPoint:local];
210
+ NSNumber *spoilerId = attributes[JMDSpoilerIDAttributeName];
211
+ NSString *url = attributes[JMDLinkURLAttributeName];
212
+
213
+ if (spoilerId != nil && ![self isSpoilerRevealed:spoilerId.integerValue]) {
214
+ // First tap reveals; links inside come alive afterwards.
215
+ [self toggleSpoiler:spoilerId.integerValue];
216
+ } else if (url != nil) {
217
+ [self linkPressed:url];
218
+ } else if (spoilerId != nil) {
219
+ [self toggleSpoiler:spoilerId.integerValue];
220
+ }
221
+ }
222
+
223
+ - (void)jmdHandleLongPress:(UILongPressGestureRecognizer *)recognizer {
224
+ if (recognizer.state != UIGestureRecognizerStateBegan) {
225
+ return;
226
+ }
227
+ CGPoint local;
228
+ UIView *view = [self jmdInteractiveViewAtPoint:[recognizer locationInView:self]
229
+ localPoint:&local];
230
+ if (![view isKindOfClass:[JMDBlockTextView class]]) {
231
+ return;
232
+ }
233
+ NSDictionary *attributes = [(JMDBlockTextView *)view attributesAtPoint:local];
234
+ NSNumber *spoilerId = attributes[JMDSpoilerIDAttributeName];
235
+ NSString *url = attributes[JMDLinkURLAttributeName];
236
+ if (url != nil && (spoilerId == nil || [self isSpoilerRevealed:spoilerId.integerValue])) {
237
+ [self linkLongPressed:url];
238
+ }
239
+ }
240
+
241
+ #pragma mark - JMDMarkdownHost
242
+
243
+ - (void)imageIntrinsicSize:(CGSize)size forUrl:(NSString *)url {
244
+ [self noteIntrinsicSize:size forUrl:url];
245
+ }
246
+
247
+ - (BOOL)isSpoilerRevealed:(NSInteger)spoilerId {
248
+ return [_revealedSpoilers containsObject:@(spoilerId)];
249
+ }
250
+
251
+ - (void)toggleSpoiler:(NSInteger)spoilerId {
252
+ if ([_revealedSpoilers containsObject:@(spoilerId)]) {
253
+ [_revealedSpoilers removeObject:@(spoilerId)];
254
+ } else {
255
+ [_revealedSpoilers addObject:@(spoilerId)];
256
+ }
257
+ JMDSetNeedsDisplayDeep(self);
258
+ }
259
+
260
+ - (const JetMarkdownViewEventEmitter *)markdownEventEmitter {
261
+ if (!_eventEmitter) {
262
+ return nullptr;
263
+ }
264
+ return static_cast<const JetMarkdownViewEventEmitter *>(_eventEmitter.get());
265
+ }
266
+
267
+ - (void)linkPressed:(NSString *)url {
268
+ if (const auto *emitter = [self markdownEventEmitter]) {
269
+ emitter->onLinkPress({.url = std::string(url.UTF8String ?: "")});
270
+ }
271
+ }
272
+
273
+ - (void)linkLongPressed:(NSString *)url {
274
+ if (const auto *emitter = [self markdownEventEmitter]) {
275
+ emitter->onLinkLongPress({.url = std::string(url.UTF8String ?: "")});
276
+ }
277
+ }
278
+
279
+ - (void)imagePressed:(NSString *)url frame:(CGRect)frame {
280
+ if (const auto *emitter = [self markdownEventEmitter]) {
281
+ emitter->onImagePress({
282
+ .height = frame.size.height,
283
+ .url = std::string(url.UTF8String ?: ""),
284
+ .width = frame.size.width,
285
+ .x = frame.origin.x,
286
+ .y = frame.origin.y,
287
+ });
288
+ }
289
+ }
290
+
291
+ static CGRect JMDScreenFrameOfView(UIView *view) {
292
+ CGRect frame = [view convertRect:view.bounds toView:nil];
293
+ UIWindow *window = view.window;
294
+ if (window != nil) {
295
+ frame = [window convertRect:frame toCoordinateSpace:window.screen.coordinateSpace];
296
+ }
297
+ return frame;
298
+ }
299
+
300
+ static void JMDSetNeedsDisplayDeep(UIView *view) {
301
+ // Spoiler covers are drawn by the text views only; invalidating images
302
+ // and scrollers too would double the redraw work.
303
+ if ([view isKindOfClass:JMDBlockTextView.class]) {
304
+ [view setNeedsDisplay];
305
+ }
306
+ for (UIView *subview in view.subviews) {
307
+ JMDSetNeedsDisplayDeep(subview);
308
+ }
309
+ }
310
+
311
+ - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
312
+ [super traitCollectionDidChange:previousTraitCollection];
313
+ if ([self.traitCollection
314
+ hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
315
+ // Dynamic (platform) colors resolve at draw time for text, but border
316
+ // and background colors snapshot into CGColors when blocks bind; rebind
317
+ // so they re-resolve under the new appearance.
318
+ _boundKey = nil;
319
+ [self setNeedsLayout];
320
+ JMDSetNeedsDisplayDeep(self);
321
+ }
322
+ }
323
+
324
+ - (void)noteIntrinsicSize:(CGSize)size forUrl:(NSString *)url {
325
+ if (_propImageSizes[url] != nil || _loadedImageSizes[url] != nil) {
326
+ return;
327
+ }
328
+ _loadedImageSizes[url] = @[ @(size.width), @(size.height) ];
329
+
330
+ // Publish into the shadow-node state so measure() grows the component.
331
+ if (_state != nullptr) {
332
+ std::map<std::string, JetMarkdownState::ImageSize> sizes;
333
+ for (NSString *key in _loadedImageSizes) {
334
+ NSArray<NSNumber *> *value = _loadedImageSizes[key];
335
+ sizes[std::string(key.UTF8String ?: "")] = JetMarkdownState::ImageSize{
336
+ value[0].doubleValue, value[1].doubleValue};
337
+ }
338
+ _state->updateState(JetMarkdownState(std::move(sizes)));
339
+ }
340
+ [self setNeedsLayout];
341
+ }
342
+
343
+ - (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState {
344
+ _state = std::static_pointer_cast<const JetMarkdownShadowNode::ConcreteState>(state);
345
+ }
346
+
347
+ - (void)dealloc {
348
+ [NSNotificationCenter.defaultCenter removeObserver:self];
349
+ }
350
+
351
+ - (void)jmdContentSizeCategoryDidChange {
352
+ if (_allowFontScaling) {
353
+ [self setNeedsLayout];
354
+ }
355
+ }
356
+
357
+ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps {
358
+ const auto &newViewProps = *std::static_pointer_cast<JetMarkdownViewProps const>(props);
359
+
360
+ NSString *markdown =
361
+ [[NSString alloc] initWithBytes:newViewProps.markdown.data()
362
+ length:newViewProps.markdown.size()
363
+ encoding:NSUTF8StringEncoding] ?: @"";
364
+ NSString *stylesJson =
365
+ [[NSString alloc] initWithBytes:newViewProps.stylesJson.data()
366
+ length:newViewProps.stylesJson.size()
367
+ encoding:NSUTF8StringEncoding] ?: @"";
368
+
369
+ if (![markdown isEqualToString:_markdown]) {
370
+ // Spoiler ids are render-order counters and learned image sizes belong
371
+ // to the old document; both must not survive a content change.
372
+ [_revealedSpoilers removeAllObjects];
373
+ [_loadedImageSizes removeAllObjects];
374
+ }
375
+ if (![markdown isEqualToString:_markdown] || ![stylesJson isEqualToString:_stylesJson] ||
376
+ newViewProps.allowFontScaling != _allowFontScaling) {
377
+ _markdown = markdown;
378
+ _stylesJson = stylesJson;
379
+ _allowFontScaling = newViewProps.allowFontScaling;
380
+ [self setNeedsLayout];
381
+ }
382
+
383
+ NSMutableDictionary<NSString *, NSArray<NSNumber *> *> *nextPropSizes =
384
+ [NSMutableDictionary dictionaryWithCapacity:newViewProps.images.size()];
385
+ for (const auto &image : newViewProps.images) {
386
+ NSString *url = [[NSString alloc] initWithBytes:image.url.data()
387
+ length:image.url.size()
388
+ encoding:NSUTF8StringEncoding];
389
+ if (url != nil) {
390
+ nextPropSizes[url] = @[ @(image.width), @(image.height) ];
391
+ }
392
+ }
393
+ if (![nextPropSizes isEqualToDictionary:_propImageSizes]) {
394
+ _propImageSizes = nextPropSizes;
395
+ // The pre-size data changed: cached layouts for the old sizes must not
396
+ // be reused even when the measured height happens to match.
397
+ [self setNeedsLayout];
398
+ }
399
+
400
+ [super updateProps:props oldProps:oldProps];
401
+ }
402
+
403
+ - (NSDictionary<NSString *, NSArray<NSNumber *> *> *)mergedImageSizes {
404
+ if (_loadedImageSizes.count == 0 && _propImageSizes.count == 0) {
405
+ return @{};
406
+ }
407
+ NSMutableDictionary *merged = [_loadedImageSizes mutableCopy];
408
+ [merged addEntriesFromDictionary:_propImageSizes];
409
+ return merged;
410
+ }
411
+
412
+ - (void)layoutSubviews {
413
+ [super layoutSubviews];
414
+ [self rebuildBlocks];
415
+ }
416
+
417
+ - (void)prepareForRecycle {
418
+ [super prepareForRecycle];
419
+ _markdown = @"";
420
+ _stylesJson = @"";
421
+ _boundKey = nil;
422
+ _state = nullptr;
423
+ [_propImageSizes removeAllObjects];
424
+ [_loadedImageSizes removeAllObjects];
425
+ [_revealedSpoilers removeAllObjects];
426
+ [_stack setBlocks:@[] gap:0];
427
+ }
428
+
429
+ - (void)rebuildBlocks {
430
+ JMDStyleConfig *styles = [JMDStyleConfig configWithJson:_stylesJson];
431
+ self.backgroundColor = styles.backgroundColor ?: UIColor.clearColor;
432
+
433
+ const CGFloat contentWidth =
434
+ self.bounds.size.width - styles.paddingLeft - styles.paddingRight;
435
+ if (contentWidth <= 0 || _markdown.length == 0) {
436
+ [_stack setBlocks:@[] gap:0];
437
+ _boundKey = nil;
438
+ return;
439
+ }
440
+
441
+ // Must match the shadow node's LayoutContext::fontSizeMultiplier.
442
+ const CGFloat fontScale = _allowFontScaling ? JMDFontSizeMultiplier() : 1.0;
443
+ JMDRenderedContent *content = [JMDContentCache contentForMarkdown:_markdown
444
+ stylesJson:_stylesJson
445
+ fontScale:fontScale];
446
+ NSDictionary *imageSizes = [self mergedImageSizes];
447
+ JMDWidthLayout *layout = [content layoutForWidth:contentWidth imageSizes:imageSizes];
448
+
449
+ // Full contents, not hashes: CFString samples long strings and
450
+ // NSDictionary.hash is the entry count — both collide.
451
+ NSString *key = [NSString stringWithFormat:@"%@\x1f%@\x1f%@\x1f%.3f",
452
+ _markdown,
453
+ _stylesJson,
454
+ [JMDRenderedContent keyForImageSizes:imageSizes],
455
+ fontScale];
456
+ if (![key isEqualToString:_boundKey] || _boundWidth != contentWidth) {
457
+ _boundKey = key;
458
+ _boundWidth = contentWidth;
459
+ [_stack setBlocks:layout.measured gap:content.gap];
460
+ }
461
+
462
+ const CGFloat contentHeight =
463
+ layout.totalHeight - styles.paddingTop - styles.paddingBottom;
464
+ _stack.frame =
465
+ CGRectMake(styles.paddingLeft, styles.paddingTop, contentWidth, contentHeight);
466
+ }
467
+
468
+ @end
@@ -0,0 +1,12 @@
1
+ #import <React/RCTViewComponentView.h>
2
+ #import <UIKit/UIKit.h>
3
+
4
+ NS_ASSUME_NONNULL_BEGIN
5
+
6
+ /// WYSIWYG markdown editor: hosts a UITextView, publishes its content
7
+ /// height into the shadow-node state (autogrow), and emits editing events.
8
+ @interface JetMarkdownEditor : RCTViewComponentView
9
+
10
+ @end
11
+
12
+ NS_ASSUME_NONNULL_END