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.
- package/JetMarkdown.podspec +42 -0
- package/LICENSE +20 -0
- package/README.md +247 -0
- package/android/CMakeLists.txt +52 -0
- package/android/build.gradle +55 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/JniBindings.cpp +243 -0
- package/android/src/main/java/com/jetmarkdown/JetMarkdownNative.kt +114 -0
- package/android/src/main/java/com/jetmarkdown/JetMarkdownPackage.kt +22 -0
- package/android/src/main/java/com/jetmarkdown/JetMarkdownView.kt +235 -0
- package/android/src/main/java/com/jetmarkdown/JetMarkdownViewManager.kt +75 -0
- package/android/src/main/java/com/jetmarkdown/editor/EditorSpans.kt +217 -0
- package/android/src/main/java/com/jetmarkdown/editor/JetMarkdownEditorManager.kt +224 -0
- package/android/src/main/java/com/jetmarkdown/editor/JetMarkdownEditorView.kt +1736 -0
- package/android/src/main/java/com/jetmarkdown/measure/MarkdownMeasurer.kt +54 -0
- package/android/src/main/java/com/jetmarkdown/parser/AstDecoder.kt +59 -0
- package/android/src/main/java/com/jetmarkdown/parser/MdNode.kt +43 -0
- package/android/src/main/java/com/jetmarkdown/render/ContentCache.kt +53 -0
- package/android/src/main/java/com/jetmarkdown/render/RenderedContent.kt +318 -0
- package/android/src/main/java/com/jetmarkdown/render/SpannableRenderer.kt +658 -0
- package/android/src/main/java/com/jetmarkdown/render/spans/ChipSpan.kt +17 -0
- package/android/src/main/java/com/jetmarkdown/render/spans/InteractiveSpans.kt +11 -0
- package/android/src/main/java/com/jetmarkdown/render/spans/MarkdownLineHeightSpan.kt +59 -0
- package/android/src/main/java/com/jetmarkdown/render/spans/RunSpan.kt +40 -0
- package/android/src/main/java/com/jetmarkdown/style/Fonts.kt +34 -0
- package/android/src/main/java/com/jetmarkdown/style/LayoutStyleSpec.kt +71 -0
- package/android/src/main/java/com/jetmarkdown/style/PlatformColorResolver.kt +77 -0
- package/android/src/main/java/com/jetmarkdown/style/StyleConfig.kt +80 -0
- package/android/src/main/java/com/jetmarkdown/style/TextStyleSpec.kt +61 -0
- package/android/src/main/java/com/jetmarkdown/views/BlockStackView.kt +292 -0
- package/android/src/main/java/com/jetmarkdown/views/BlockTextView.kt +378 -0
- package/android/src/main/java/com/jetmarkdown/views/BoxDrawing.kt +50 -0
- package/android/src/main/java/com/jetmarkdown/views/MarkdownHost.kt +12 -0
- package/android/src/main/java/com/jetmarkdown/views/MarkdownImageView.kt +113 -0
- package/android/src/main/java/com/jetmarkdown/views/NestedHorizontalScrollView.kt +87 -0
- package/android/src/main/java/com/jetmarkdown/views/TableBlockView.kt +139 -0
- package/cpp/core/Ast.h +85 -0
- package/cpp/core/AstJson.cpp +129 -0
- package/cpp/core/AstJson.h +12 -0
- package/cpp/core/AstSerializer.cpp +40 -0
- package/cpp/core/AstSerializer.h +21 -0
- package/cpp/core/AstToMarkdown.cpp +549 -0
- package/cpp/core/AstToMarkdown.h +16 -0
- package/cpp/core/EditorRuns.cpp +640 -0
- package/cpp/core/EditorRuns.h +94 -0
- package/cpp/core/EditorText.cpp +15 -0
- package/cpp/core/EditorText.h +18 -0
- package/cpp/core/InlineExtensions.cpp +305 -0
- package/cpp/core/InlineExtensions.h +17 -0
- package/cpp/core/Parser.cpp +470 -0
- package/cpp/core/Parser.h +16 -0
- package/cpp/core/Preprocess.cpp +181 -0
- package/cpp/core/Preprocess.h +14 -0
- package/cpp/md4c/LICENSE +22 -0
- package/cpp/md4c/VERSION +1 -0
- package/cpp/md4c/md4c.c +6462 -0
- package/cpp/md4c/md4c.h +407 -0
- package/cpp/react/JetMarkdownEditorShadowNode.cpp +54 -0
- package/cpp/react/JetMarkdownEditorShadowNode.h +55 -0
- package/cpp/react/JetMarkdownEditorState.h +35 -0
- package/cpp/react/JetMarkdownMeasurer.cpp +32 -0
- package/cpp/react/JetMarkdownMeasurer.h +41 -0
- package/cpp/react/JetMarkdownShadowNode.cpp +100 -0
- package/cpp/react/JetMarkdownShadowNode.h +53 -0
- package/cpp/react/JetMarkdownState.h +52 -0
- package/cpp/react/override/react/renderer/components/JetMarkdownViewSpec/ComponentDescriptors.h +21 -0
- package/cpp/tests/parser_tests.cpp +662 -0
- package/cpp/tests/run_tests.sh +25 -0
- package/ios/JetMarkdownView.h +14 -0
- package/ios/JetMarkdownView.mm +468 -0
- package/ios/editor/JetMarkdownEditor.h +12 -0
- package/ios/editor/JetMarkdownEditor.mm +2002 -0
- package/ios/measure/JMDMarkdownMeasurer.h +22 -0
- package/ios/measure/JMDMarkdownMeasurer.mm +38 -0
- package/ios/render/JMDBlock.h +97 -0
- package/ios/render/JMDBlock.m +28 -0
- package/ios/render/JMDBlockRenderer.h +17 -0
- package/ios/render/JMDBlockRenderer.mm +852 -0
- package/ios/render/JMDContentCache.h +17 -0
- package/ios/render/JMDContentCache.mm +42 -0
- package/ios/render/JMDRenderedContent.h +35 -0
- package/ios/render/JMDRenderedContent.mm +274 -0
- package/ios/style/JMDFontScale.h +33 -0
- package/ios/style/JMDLayoutStyle.h +37 -0
- package/ios/style/JMDLayoutStyle.mm +68 -0
- package/ios/style/JMDStyleConfig.h +39 -0
- package/ios/style/JMDStyleConfig.mm +111 -0
- package/ios/style/JMDTextStyle.h +25 -0
- package/ios/style/JMDTextStyle.mm +68 -0
- package/ios/views/JMDBlockStackView.h +18 -0
- package/ios/views/JMDBlockStackView.m +97 -0
- package/ios/views/JMDBlockTextView.h +24 -0
- package/ios/views/JMDBlockTextView.m +340 -0
- package/ios/views/JMDBoxViews.h +33 -0
- package/ios/views/JMDBoxViews.m +292 -0
- package/ios/views/JMDImageView.h +19 -0
- package/ios/views/JMDImageView.m +68 -0
- package/ios/views/JMDMarkdownHost.h +16 -0
- package/ios/views/JMDTableView.h +16 -0
- package/ios/views/JMDTableView.m +172 -0
- package/lib/module/JetMarkdownEditor.js +85 -0
- package/lib/module/JetMarkdownEditor.js.map +1 -0
- package/lib/module/JetMarkdownEditor.native.js +244 -0
- package/lib/module/JetMarkdownEditor.native.js.map +1 -0
- package/lib/module/JetMarkdownEditorNativeComponent.ts +173 -0
- package/lib/module/JetMarkdownView.js +19 -0
- package/lib/module/JetMarkdownView.js.map +1 -0
- package/lib/module/JetMarkdownView.native.js +44 -0
- package/lib/module/JetMarkdownView.native.js.map +1 -0
- package/lib/module/JetMarkdownViewNativeComponent.ts +35 -0
- package/lib/module/defaultStyles.js +122 -0
- package/lib/module/defaultStyles.js.map +1 -0
- package/lib/module/index.js +9 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/serializeStyles.js +259 -0
- package/lib/module/serializeStyles.js.map +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/useJetMarkdownEditor.js +41 -0
- package/lib/module/useJetMarkdownEditor.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/JetMarkdownEditor.d.ts +6 -0
- package/lib/typescript/src/JetMarkdownEditor.d.ts.map +1 -0
- package/lib/typescript/src/JetMarkdownEditor.native.d.ts +6 -0
- package/lib/typescript/src/JetMarkdownEditor.native.d.ts.map +1 -0
- package/lib/typescript/src/JetMarkdownEditorNativeComponent.d.ts +105 -0
- package/lib/typescript/src/JetMarkdownEditorNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/JetMarkdownView.d.ts +3 -0
- package/lib/typescript/src/JetMarkdownView.d.ts.map +1 -0
- package/lib/typescript/src/JetMarkdownView.native.d.ts +3 -0
- package/lib/typescript/src/JetMarkdownView.native.d.ts.map +1 -0
- package/lib/typescript/src/JetMarkdownViewNativeComponent.d.ts +28 -0
- package/lib/typescript/src/JetMarkdownViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/defaultStyles.d.ts +20 -0
- package/lib/typescript/src/defaultStyles.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +7 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/serializeStyles.d.ts +34 -0
- package/lib/typescript/src/serializeStyles.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +352 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/useJetMarkdownEditor.d.ts +36 -0
- package/lib/typescript/src/useJetMarkdownEditor.d.ts.map +1 -0
- package/package.json +150 -0
- package/react-native.config.js +11 -0
- package/src/JetMarkdownEditor.native.tsx +301 -0
- package/src/JetMarkdownEditor.tsx +90 -0
- package/src/JetMarkdownEditorNativeComponent.ts +173 -0
- package/src/JetMarkdownView.native.tsx +67 -0
- package/src/JetMarkdownView.tsx +16 -0
- package/src/JetMarkdownViewNativeComponent.ts +35 -0
- package/src/defaultStyles.ts +102 -0
- package/src/index.tsx +34 -0
- package/src/serializeStyles.ts +369 -0
- package/src/types.ts +438 -0
- 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
|