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,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
|