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,852 @@
|
|
|
1
|
+
#import "JMDBlockRenderer.h"
|
|
2
|
+
|
|
3
|
+
#import <CoreText/CoreText.h>
|
|
4
|
+
|
|
5
|
+
#import "core/Parser.h"
|
|
6
|
+
|
|
7
|
+
using jetmarkdown::Node;
|
|
8
|
+
using jetmarkdown::NodeType;
|
|
9
|
+
|
|
10
|
+
NSAttributedStringKey const JMDLinkURLAttributeName = @"JMDLinkURL";
|
|
11
|
+
NSAttributedStringKey const JMDRunBackgroundAttributeName = @"JMDRunBackground";
|
|
12
|
+
NSAttributedStringKey const JMDSpoilerIDAttributeName = @"JMDSpoilerID";
|
|
13
|
+
|
|
14
|
+
namespace {
|
|
15
|
+
|
|
16
|
+
NSString *toNSString(const std::string &value) {
|
|
17
|
+
NSString *result = [[NSString alloc] initWithBytes:value.data()
|
|
18
|
+
length:value.size()
|
|
19
|
+
encoding:NSUTF8StringEncoding];
|
|
20
|
+
return result != nil ? result : @"";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
bool isInlineType(NodeType type) {
|
|
24
|
+
switch (type) {
|
|
25
|
+
case NodeType::Text:
|
|
26
|
+
case NodeType::SoftBreak:
|
|
27
|
+
case NodeType::HardBreak:
|
|
28
|
+
case NodeType::Bold:
|
|
29
|
+
case NodeType::Italic:
|
|
30
|
+
case NodeType::Strikethrough:
|
|
31
|
+
case NodeType::Link:
|
|
32
|
+
case NodeType::InlineCode:
|
|
33
|
+
case NodeType::Spoiler:
|
|
34
|
+
case NodeType::Superscript:
|
|
35
|
+
case NodeType::Subscript:
|
|
36
|
+
case NodeType::Image:
|
|
37
|
+
return true;
|
|
38
|
+
default:
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// codeBlock/blockQuote merge text + layout keys in one section; their
|
|
44
|
+
// backgroundColor is the box fill, not an inline-run background, so it must
|
|
45
|
+
// not reach the text-attribute path.
|
|
46
|
+
JMDTextStyle *JMDTextStyleWithoutBackground(JMDStyleConfig *styles, NSString *key) {
|
|
47
|
+
NSDictionary *section = [styles rawSectionFor:key];
|
|
48
|
+
if (section == nil) {
|
|
49
|
+
return nil;
|
|
50
|
+
}
|
|
51
|
+
if (section[@"backgroundColor"] == nil) {
|
|
52
|
+
return [JMDTextStyle fromJson:section];
|
|
53
|
+
}
|
|
54
|
+
NSMutableDictionary *copy = [section mutableCopy];
|
|
55
|
+
[copy removeObjectForKey:@"backgroundColor"];
|
|
56
|
+
return [JMDTextStyle fromJson:copy];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Fully-resolved text attributes at one point of the inline tree walk.
|
|
60
|
+
struct ResolvedAttrs {
|
|
61
|
+
CGFloat fontSize = 16;
|
|
62
|
+
CGFloat lineHeight = 0; // 0 = natural
|
|
63
|
+
NSInteger weight = 400;
|
|
64
|
+
bool italic = false;
|
|
65
|
+
NSString *__strong family = nil;
|
|
66
|
+
UIColor *__strong color = nil;
|
|
67
|
+
NSArray<NSString *> *__strong variants = nil;
|
|
68
|
+
bool underline = false;
|
|
69
|
+
bool strikethrough = false;
|
|
70
|
+
UIColor *__strong decorationColor = nil;
|
|
71
|
+
NSString *__strong decorationStyle = nil;
|
|
72
|
+
CGFloat baselineOffset = 0;
|
|
73
|
+
// lineHeight-centering raise, computed ONCE from the block's base font so
|
|
74
|
+
// every run on a line shares the same shift (per-run deltas cancel sub/sup
|
|
75
|
+
// offsets and split mixed-font runs onto different visual baselines).
|
|
76
|
+
CGFloat centeringOffset = 0;
|
|
77
|
+
UIColor *__strong backgroundColor = nil;
|
|
78
|
+
// Chip geometry for drawn run backgrounds (inlineCode/link/mention).
|
|
79
|
+
CGFloat chipRadius = 0;
|
|
80
|
+
bool chipContinuous = false;
|
|
81
|
+
CGFloat chipPadLeft = 0;
|
|
82
|
+
CGFloat chipPadRight = 0;
|
|
83
|
+
// One chip object per link/mention node, shared by its child runs so
|
|
84
|
+
// enumerateAttribute coalesces them into ONE chip — per-run instances
|
|
85
|
+
// fragment a mixed-styling link into seamed per-run rects.
|
|
86
|
+
JMDRunBackground *__strong chipInstance = nil;
|
|
87
|
+
NSString *__strong linkUrl = nil;
|
|
88
|
+
NSInteger spoilerId = -1;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
void applyStyle(ResolvedAttrs &attrs, JMDTextStyle *style, CGFloat fontScale) {
|
|
92
|
+
if (style == nil) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (style.fontSize != nil) {
|
|
96
|
+
attrs.fontSize = style.fontSize.doubleValue * fontScale;
|
|
97
|
+
}
|
|
98
|
+
if (style.fontWeight != nil) {
|
|
99
|
+
attrs.weight = style.fontWeight.integerValue;
|
|
100
|
+
}
|
|
101
|
+
if (style.fontFamily != nil) {
|
|
102
|
+
attrs.family = style.fontFamily;
|
|
103
|
+
}
|
|
104
|
+
if (style.lineHeight != nil) {
|
|
105
|
+
attrs.lineHeight = style.lineHeight.doubleValue * fontScale;
|
|
106
|
+
}
|
|
107
|
+
if (style.color != nil) {
|
|
108
|
+
attrs.color = style.color;
|
|
109
|
+
}
|
|
110
|
+
if (style.fontVariant != nil) {
|
|
111
|
+
attrs.variants = style.fontVariant;
|
|
112
|
+
}
|
|
113
|
+
if (style.textDecorationColor != nil) {
|
|
114
|
+
attrs.decorationColor = style.textDecorationColor;
|
|
115
|
+
}
|
|
116
|
+
if (style.textDecorationStyle != nil) {
|
|
117
|
+
attrs.decorationStyle = style.textDecorationStyle;
|
|
118
|
+
}
|
|
119
|
+
if (style.textDecorationLine != nil) {
|
|
120
|
+
attrs.underline = [style.textDecorationLine containsString:@"underline"];
|
|
121
|
+
attrs.strikethrough = [style.textDecorationLine containsString:@"line-through"];
|
|
122
|
+
}
|
|
123
|
+
if (style.backgroundColor != nil) {
|
|
124
|
+
attrs.backgroundColor = style.backgroundColor;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
void applyChipStyle(ResolvedAttrs &attrs, NSDictionary *section) {
|
|
129
|
+
if (section == nil) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if ([section[@"borderRadius"] isKindOfClass:[NSNumber class]]) {
|
|
133
|
+
attrs.chipRadius = [section[@"borderRadius"] doubleValue];
|
|
134
|
+
}
|
|
135
|
+
attrs.chipContinuous = [section[@"borderCurve"] isEqual:@"continuous"];
|
|
136
|
+
if ([section[@"paddingLeft"] isKindOfClass:[NSNumber class]]) {
|
|
137
|
+
attrs.chipPadLeft = [section[@"paddingLeft"] doubleValue];
|
|
138
|
+
}
|
|
139
|
+
if ([section[@"paddingRight"] isKindOfClass:[NSNumber class]]) {
|
|
140
|
+
attrs.chipPadRight = [section[@"paddingRight"] doubleValue];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
UIFontWeight uiWeight(NSInteger weight) {
|
|
145
|
+
switch (weight) {
|
|
146
|
+
case 100: return UIFontWeightUltraLight;
|
|
147
|
+
case 200: return UIFontWeightThin;
|
|
148
|
+
case 300: return UIFontWeightLight;
|
|
149
|
+
case 400: return UIFontWeightRegular;
|
|
150
|
+
case 500: return UIFontWeightMedium;
|
|
151
|
+
case 600: return UIFontWeightSemibold;
|
|
152
|
+
case 700: return UIFontWeightBold;
|
|
153
|
+
case 800: return UIFontWeightHeavy;
|
|
154
|
+
default: return weight >= 900 ? UIFontWeightBlack : UIFontWeightRegular;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
NSArray<NSDictionary *> *featureSettings(NSArray<NSString *> *variants) {
|
|
159
|
+
NSMutableArray *features = [NSMutableArray new];
|
|
160
|
+
for (NSString *variant in variants) {
|
|
161
|
+
int type = -1;
|
|
162
|
+
int selector = -1;
|
|
163
|
+
if ([variant isEqualToString:@"tabular-nums"]) {
|
|
164
|
+
type = kNumberSpacingType;
|
|
165
|
+
selector = kMonospacedNumbersSelector;
|
|
166
|
+
} else if ([variant isEqualToString:@"proportional-nums"]) {
|
|
167
|
+
type = kNumberSpacingType;
|
|
168
|
+
selector = kProportionalNumbersSelector;
|
|
169
|
+
} else if ([variant isEqualToString:@"oldstyle-nums"]) {
|
|
170
|
+
type = kNumberCaseType;
|
|
171
|
+
selector = kLowerCaseNumbersSelector;
|
|
172
|
+
} else if ([variant isEqualToString:@"lining-nums"]) {
|
|
173
|
+
type = kNumberCaseType;
|
|
174
|
+
selector = kUpperCaseNumbersSelector;
|
|
175
|
+
} else if ([variant isEqualToString:@"small-caps"]) {
|
|
176
|
+
type = kLowerCaseType;
|
|
177
|
+
selector = kLowerCaseSmallCapsSelector;
|
|
178
|
+
}
|
|
179
|
+
if (type >= 0) {
|
|
180
|
+
[features addObject:@{
|
|
181
|
+
UIFontFeatureTypeIdentifierKey : @(type),
|
|
182
|
+
UIFontFeatureSelectorIdentifierKey : @(selector),
|
|
183
|
+
}];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return features;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
UIFont *buildFont(const ResolvedAttrs &attrs) {
|
|
190
|
+
UIFont *base;
|
|
191
|
+
if (attrs.family.length > 0) {
|
|
192
|
+
UIFontDescriptor *descriptor = [UIFontDescriptor fontDescriptorWithFontAttributes:@{
|
|
193
|
+
UIFontDescriptorFamilyAttribute : attrs.family,
|
|
194
|
+
UIFontDescriptorTraitsAttribute : @{
|
|
195
|
+
UIFontWeightTrait : @((uiWeight(attrs.weight) - UIFontWeightRegular)),
|
|
196
|
+
},
|
|
197
|
+
}];
|
|
198
|
+
base = [UIFont fontWithDescriptor:descriptor size:attrs.fontSize];
|
|
199
|
+
if (base == nil) {
|
|
200
|
+
base = [UIFont systemFontOfSize:attrs.fontSize weight:uiWeight(attrs.weight)];
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
base = [UIFont systemFontOfSize:attrs.fontSize weight:uiWeight(attrs.weight)];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
UIFontDescriptorSymbolicTraits traits = base.fontDescriptor.symbolicTraits;
|
|
207
|
+
if (attrs.italic) {
|
|
208
|
+
traits |= UIFontDescriptorTraitItalic;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
NSMutableDictionary *descriptorAttributes = [NSMutableDictionary new];
|
|
212
|
+
NSArray *features = attrs.variants != nil ? featureSettings(attrs.variants) : @[];
|
|
213
|
+
if (features.count > 0) {
|
|
214
|
+
descriptorAttributes[UIFontDescriptorFeatureSettingsAttribute] = features;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
UIFontDescriptor *descriptor = base.fontDescriptor;
|
|
218
|
+
if (descriptorAttributes.count > 0) {
|
|
219
|
+
descriptor = [descriptor fontDescriptorByAddingAttributes:descriptorAttributes];
|
|
220
|
+
}
|
|
221
|
+
if (attrs.italic) {
|
|
222
|
+
UIFontDescriptor *italicDescriptor = [descriptor fontDescriptorWithSymbolicTraits:traits];
|
|
223
|
+
if (italicDescriptor != nil) {
|
|
224
|
+
descriptor = italicDescriptor;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
UIFont *font = [UIFont fontWithDescriptor:descriptor size:attrs.fontSize];
|
|
228
|
+
return font ?: base;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
NSUnderlineStyle decorationMask(NSString *style) {
|
|
232
|
+
if ([style isEqualToString:@"double"]) {
|
|
233
|
+
return NSUnderlineStyleDouble;
|
|
234
|
+
}
|
|
235
|
+
if ([style isEqualToString:@"dotted"]) {
|
|
236
|
+
return NSUnderlineStyleSingle | NSUnderlineStylePatternDot;
|
|
237
|
+
}
|
|
238
|
+
if ([style isEqualToString:@"dashed"]) {
|
|
239
|
+
return NSUnderlineStyleSingle | NSUnderlineStylePatternDash;
|
|
240
|
+
}
|
|
241
|
+
return NSUnderlineStyleSingle;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// RN semantics: the line box is exactly lineHeight tall and glyphs center
|
|
245
|
+
// vertically inside it. The raise derives from the BLOCK's base font; call
|
|
246
|
+
// once wherever block-level attrs are finalized.
|
|
247
|
+
void applyCenteringOffset(ResolvedAttrs &attrs) {
|
|
248
|
+
attrs.centeringOffset = 0;
|
|
249
|
+
if (attrs.lineHeight <= 0) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
UIFont *font = buildFont(attrs);
|
|
253
|
+
const CGFloat delta = attrs.lineHeight - font.lineHeight;
|
|
254
|
+
if (delta > 0) {
|
|
255
|
+
attrs.centeringOffset = delta / 2;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
NSDictionary *attributesDictionary(const ResolvedAttrs &attrs) {
|
|
260
|
+
NSMutableDictionary *attributes = [NSMutableDictionary new];
|
|
261
|
+
UIFont *font = buildFont(attrs);
|
|
262
|
+
attributes[NSFontAttributeName] = font;
|
|
263
|
+
attributes[NSForegroundColorAttributeName] = attrs.color ?: UIColor.blackColor;
|
|
264
|
+
CGFloat baselineOffset = attrs.baselineOffset + attrs.centeringOffset;
|
|
265
|
+
if (attrs.lineHeight > 0) {
|
|
266
|
+
NSMutableParagraphStyle *paragraph = [NSMutableParagraphStyle new];
|
|
267
|
+
paragraph.minimumLineHeight = attrs.lineHeight;
|
|
268
|
+
paragraph.maximumLineHeight = attrs.lineHeight;
|
|
269
|
+
attributes[NSParagraphStyleAttributeName] = paragraph;
|
|
270
|
+
}
|
|
271
|
+
if (attrs.underline) {
|
|
272
|
+
attributes[NSUnderlineStyleAttributeName] = @(decorationMask(attrs.decorationStyle));
|
|
273
|
+
if (attrs.decorationColor != nil) {
|
|
274
|
+
attributes[NSUnderlineColorAttributeName] = attrs.decorationColor;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (attrs.strikethrough) {
|
|
278
|
+
attributes[NSStrikethroughStyleAttributeName] = @(decorationMask(attrs.decorationStyle));
|
|
279
|
+
if (attrs.decorationColor != nil) {
|
|
280
|
+
attributes[NSStrikethroughColorAttributeName] = attrs.decorationColor;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (baselineOffset != 0) {
|
|
284
|
+
attributes[NSBaselineOffsetAttributeName] = @(baselineOffset);
|
|
285
|
+
}
|
|
286
|
+
if (attrs.chipInstance != nil) {
|
|
287
|
+
attributes[JMDRunBackgroundAttributeName] = attrs.chipInstance;
|
|
288
|
+
} else if (attrs.backgroundColor != nil) {
|
|
289
|
+
JMDRunBackground *chip = [JMDRunBackground new];
|
|
290
|
+
chip.color = attrs.backgroundColor;
|
|
291
|
+
chip.radius = attrs.chipRadius;
|
|
292
|
+
chip.continuousCurve = attrs.chipContinuous;
|
|
293
|
+
chip.padLeft = attrs.chipPadLeft;
|
|
294
|
+
chip.padRight = attrs.chipPadRight;
|
|
295
|
+
attributes[JMDRunBackgroundAttributeName] = chip;
|
|
296
|
+
}
|
|
297
|
+
if (attrs.linkUrl != nil) {
|
|
298
|
+
attributes[JMDLinkURLAttributeName] = attrs.linkUrl;
|
|
299
|
+
}
|
|
300
|
+
if (attrs.spoilerId >= 0) {
|
|
301
|
+
attributes[JMDSpoilerIDAttributeName] = @(attrs.spoilerId);
|
|
302
|
+
}
|
|
303
|
+
return attributes;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
class BlockBuilder {
|
|
307
|
+
public:
|
|
308
|
+
BlockBuilder(JMDStyleConfig *styles, CGFloat fontScale)
|
|
309
|
+
: styles_(styles), fontScale_(fontScale) {}
|
|
310
|
+
|
|
311
|
+
NSArray<JMDBlock *> *renderBlocks(
|
|
312
|
+
const std::vector<Node *> &children,
|
|
313
|
+
NSArray<JMDTextStyle *> *inherited) {
|
|
314
|
+
NSMutableArray<JMDBlock *> *out = [NSMutableArray new];
|
|
315
|
+
std::vector<const Node *> inlineRun;
|
|
316
|
+
|
|
317
|
+
auto flushInline = [&]() {
|
|
318
|
+
if (!inlineRun.empty()) {
|
|
319
|
+
Node synthetic;
|
|
320
|
+
synthetic.type = NodeType::Paragraph;
|
|
321
|
+
for (const Node *node : inlineRun) {
|
|
322
|
+
synthetic.children.push_back(const_cast<Node *>(node));
|
|
323
|
+
}
|
|
324
|
+
[out addObject:textBlock(&synthetic, inherited)];
|
|
325
|
+
inlineRun.clear();
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
for (const Node *child : children) {
|
|
330
|
+
if (isInlineType(child->type)) {
|
|
331
|
+
inlineRun.push_back(child);
|
|
332
|
+
} else {
|
|
333
|
+
flushInline();
|
|
334
|
+
renderBlock(child, inherited, out);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
flushInline();
|
|
338
|
+
return out;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// -- block level ---------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
void renderBlock(
|
|
344
|
+
const Node *node,
|
|
345
|
+
NSArray<JMDTextStyle *> *inherited,
|
|
346
|
+
NSMutableArray<JMDBlock *> *out) {
|
|
347
|
+
switch (node->type) {
|
|
348
|
+
case NodeType::Paragraph:
|
|
349
|
+
case NodeType::Heading: {
|
|
350
|
+
const Node *image = singleImageChild(node);
|
|
351
|
+
if (image != nullptr) {
|
|
352
|
+
[out addObject:imageBlock(image)];
|
|
353
|
+
} else {
|
|
354
|
+
[out addObject:textBlock(node, inherited)];
|
|
355
|
+
}
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
case NodeType::BlockQuote: {
|
|
360
|
+
JMDLayoutStyle *defaults = [JMDLayoutStyle
|
|
361
|
+
defaultsWithBackground:nil
|
|
362
|
+
padding:UIEdgeInsetsZero
|
|
363
|
+
borderRadius:0
|
|
364
|
+
borderLeftColor:nil
|
|
365
|
+
borderLeftWidth:0];
|
|
366
|
+
JMDLayoutStyle *layout =
|
|
367
|
+
[JMDLayoutStyle fromJson:[styles_ rawSectionFor:@"blockQuote"] defaults:defaults];
|
|
368
|
+
NSArray<JMDTextStyle *> *quoteInherited =
|
|
369
|
+
appendStyle(inherited, JMDTextStyleWithoutBackground(styles_, @"blockQuote"));
|
|
370
|
+
JMDBlock *block = [JMDBlock new];
|
|
371
|
+
block.kind = JMDBlockKindQuote;
|
|
372
|
+
block.layoutStyle = layout;
|
|
373
|
+
block.children = renderBlocks(node->children, quoteInherited);
|
|
374
|
+
[out addObject:block];
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
case NodeType::CodeBlock: {
|
|
379
|
+
JMDLayoutStyle *defaults = [JMDLayoutStyle
|
|
380
|
+
defaultsWithBackground:nil
|
|
381
|
+
padding:UIEdgeInsetsZero
|
|
382
|
+
borderRadius:0
|
|
383
|
+
borderLeftColor:nil
|
|
384
|
+
borderLeftWidth:0];
|
|
385
|
+
JMDLayoutStyle *layout =
|
|
386
|
+
[JMDLayoutStyle fromJson:[styles_ rawSectionFor:@"codeBlock"] defaults:defaults];
|
|
387
|
+
|
|
388
|
+
ResolvedAttrs attrs;
|
|
389
|
+
attrs.fontSize = 16 * fontScale_;
|
|
390
|
+
// Base cascades into code; the monospace family is semantic and
|
|
391
|
+
// only styles.codeBlock overrides it.
|
|
392
|
+
applyStyle(attrs, [styles_ textStyleFor:@"base"], fontScale_);
|
|
393
|
+
attrs.family = @"Menlo";
|
|
394
|
+
if (attrs.color == nil) {
|
|
395
|
+
attrs.color = UIColor.blackColor;
|
|
396
|
+
}
|
|
397
|
+
for (JMDTextStyle *style in inherited) {
|
|
398
|
+
applyStyle(attrs, style, fontScale_);
|
|
399
|
+
}
|
|
400
|
+
applyStyle(attrs, JMDTextStyleWithoutBackground(styles_, @"codeBlock"), fontScale_);
|
|
401
|
+
applyCenteringOffset(attrs);
|
|
402
|
+
|
|
403
|
+
std::string text = node->text;
|
|
404
|
+
while (!text.empty() && text.back() == '\n') {
|
|
405
|
+
text.pop_back();
|
|
406
|
+
}
|
|
407
|
+
JMDBlock *block = [JMDBlock new];
|
|
408
|
+
block.kind = JMDBlockKindCode;
|
|
409
|
+
block.layoutStyle = layout;
|
|
410
|
+
block.attributedText =
|
|
411
|
+
[[NSAttributedString alloc] initWithString:toNSString(text)
|
|
412
|
+
attributes:attributesDictionary(attrs)];
|
|
413
|
+
[out addObject:block];
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
case NodeType::List:
|
|
418
|
+
[out addObject:listBlock(node, inherited)];
|
|
419
|
+
break;
|
|
420
|
+
|
|
421
|
+
case NodeType::Table:
|
|
422
|
+
[out addObject:tableBlock(node, inherited)];
|
|
423
|
+
break;
|
|
424
|
+
|
|
425
|
+
case NodeType::ThematicBreak: {
|
|
426
|
+
NSDictionary *section = [styles_ rawSectionFor:@"divider"];
|
|
427
|
+
NSNumber *height = [section[@"height"] isKindOfClass:[NSNumber class]]
|
|
428
|
+
? section[@"height"]
|
|
429
|
+
: @1;
|
|
430
|
+
JMDBlock *block = [JMDBlock new];
|
|
431
|
+
block.kind = JMDBlockKindDivider;
|
|
432
|
+
// Neutral functional floor — a divider is content, so it stays
|
|
433
|
+
// visible even unstyled; defaultStyles provides the subtle hairline.
|
|
434
|
+
block.dividerColor = [JMDTextStyle colorFromJson:section[@"color"]]
|
|
435
|
+
?: UIColor.blackColor;
|
|
436
|
+
block.dividerThickness = height.doubleValue;
|
|
437
|
+
[out addObject:block];
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
default: {
|
|
442
|
+
if (!node->children.empty()) {
|
|
443
|
+
[out addObjectsFromArray:renderBlocks(node->children, inherited)];
|
|
444
|
+
} else if (!node->text.empty()) {
|
|
445
|
+
Node text;
|
|
446
|
+
text.type = NodeType::Text;
|
|
447
|
+
text.text = node->text;
|
|
448
|
+
Node wrapper;
|
|
449
|
+
wrapper.type = NodeType::Paragraph;
|
|
450
|
+
wrapper.children.push_back(&text);
|
|
451
|
+
[out addObject:textBlock(&wrapper, inherited)];
|
|
452
|
+
}
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// A paragraph whose only meaningful child is one image renders as an
|
|
459
|
+
// image block (markdown images are inline; block display matches usage).
|
|
460
|
+
static const Node *singleImageChild(const Node *node) {
|
|
461
|
+
if (node->type != NodeType::Paragraph) {
|
|
462
|
+
return nullptr;
|
|
463
|
+
}
|
|
464
|
+
const Node *image = nullptr;
|
|
465
|
+
for (const Node *child : node->children) {
|
|
466
|
+
switch (child->type) {
|
|
467
|
+
case NodeType::Image:
|
|
468
|
+
if (image != nullptr) {
|
|
469
|
+
return nullptr;
|
|
470
|
+
}
|
|
471
|
+
image = child;
|
|
472
|
+
break;
|
|
473
|
+
case NodeType::Text: {
|
|
474
|
+
for (char c : child->text) {
|
|
475
|
+
if (c != ' ' && c != '\t') {
|
|
476
|
+
return nullptr;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
case NodeType::SoftBreak:
|
|
482
|
+
case NodeType::HardBreak:
|
|
483
|
+
break;
|
|
484
|
+
default:
|
|
485
|
+
return nullptr;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return image;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
JMDBlock *imageBlock(const Node *node) {
|
|
492
|
+
NSDictionary *section = [styles_ rawSectionFor:@"image"];
|
|
493
|
+
auto number = [](NSDictionary *dict, NSString *key, CGFloat fallback) -> CGFloat {
|
|
494
|
+
NSNumber *value = [dict[key] isKindOfClass:[NSNumber class]] ? dict[key] : nil;
|
|
495
|
+
return value != nil ? value.doubleValue : fallback;
|
|
496
|
+
};
|
|
497
|
+
JMDBlock *block = [JMDBlock new];
|
|
498
|
+
block.kind = JMDBlockKindImage;
|
|
499
|
+
block.imageUrl = toNSString(node->url);
|
|
500
|
+
block.imageBackground = [JMDTextStyle colorFromJson:section[@"backgroundColor"]];
|
|
501
|
+
block.imageBorderRadius = number(section, @"borderRadius", 0);
|
|
502
|
+
block.imageHeight = number(section, @"height", 0);
|
|
503
|
+
block.imageMaxHeight = number(section, @"maxHeight", 0);
|
|
504
|
+
block.imagePlaceholder = 200;
|
|
505
|
+
return block;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
JMDBlock *listBlock(const Node *node, NSArray<JMDTextStyle *> *inherited) {
|
|
509
|
+
NSDictionary *listSection = [styles_ rawSectionFor:@"list"];
|
|
510
|
+
NSDictionary *markerSection = [styles_ rawSectionFor:@"listMarker"];
|
|
511
|
+
|
|
512
|
+
auto number = [](NSDictionary *dict, NSString *key, CGFloat fallback) -> CGFloat {
|
|
513
|
+
NSNumber *value = [dict[key] isKindOfClass:[NSNumber class]] ? dict[key] : nil;
|
|
514
|
+
return value != nil ? value.doubleValue : fallback;
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
JMDBlock *block = [JMDBlock new];
|
|
518
|
+
block.kind = JMDBlockKindList;
|
|
519
|
+
block.listMarginLeft = number(listSection, @"marginLeft", 0);
|
|
520
|
+
block.markerMarginLeft = number(markerSection, @"marginLeft", 0);
|
|
521
|
+
|
|
522
|
+
NSArray<JMDTextStyle *> *itemInherited =
|
|
523
|
+
appendStyle(inherited, [styles_ textStyleFor:@"listItem"]);
|
|
524
|
+
|
|
525
|
+
ResolvedAttrs markerAttrs;
|
|
526
|
+
markerAttrs.fontSize = 16 * fontScale_;
|
|
527
|
+
markerAttrs.color = UIColor.blackColor;
|
|
528
|
+
applyStyle(markerAttrs, [styles_ textStyleFor:@"base"], fontScale_);
|
|
529
|
+
applyStyle(markerAttrs, [styles_ textStyleFor:@"paragraph"], fontScale_);
|
|
530
|
+
for (JMDTextStyle *style in itemInherited) {
|
|
531
|
+
applyStyle(markerAttrs, style, fontScale_);
|
|
532
|
+
}
|
|
533
|
+
UIColor *markerColor = [JMDTextStyle colorFromJson:markerSection[@"color"]];
|
|
534
|
+
if (markerColor != nil) {
|
|
535
|
+
markerAttrs.color = markerColor;
|
|
536
|
+
}
|
|
537
|
+
applyCenteringOffset(markerAttrs);
|
|
538
|
+
|
|
539
|
+
NSMutableArray<JMDListRow *> *rows = [NSMutableArray new];
|
|
540
|
+
int index = node->startIndex;
|
|
541
|
+
CGFloat naturalMarkerWidth = 0;
|
|
542
|
+
for (const Node *item : node->children) {
|
|
543
|
+
if (item->type != NodeType::ListItem) {
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
NSString *markerText =
|
|
547
|
+
node->ordered ? [NSString stringWithFormat:@"%d.", index] : @"•";
|
|
548
|
+
JMDListRow *row = [JMDListRow new];
|
|
549
|
+
row.marker = [[NSAttributedString alloc] initWithString:markerText
|
|
550
|
+
attributes:attributesDictionary(markerAttrs)];
|
|
551
|
+
naturalMarkerWidth = MAX(naturalMarkerWidth, ceil(row.marker.size.width));
|
|
552
|
+
row.content = renderBlocks(item->children, itemInherited);
|
|
553
|
+
[rows addObject:row];
|
|
554
|
+
index++;
|
|
555
|
+
}
|
|
556
|
+
// Unstyled marker column is content-driven (widest marker); defaultStyles
|
|
557
|
+
// provides the classic fixed width.
|
|
558
|
+
const CGFloat styledWidth = number(markerSection, @"width", -1);
|
|
559
|
+
block.markerWidth = styledWidth >= 0 ? styledWidth : naturalMarkerWidth;
|
|
560
|
+
block.rows = rows;
|
|
561
|
+
return block;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
JMDBlock *tableBlock(const Node *node, NSArray<JMDTextStyle *> *inherited) {
|
|
565
|
+
NSDictionary *tableSection = [styles_ rawSectionFor:@"table"];
|
|
566
|
+
NSDictionary *cellSection = [styles_ rawSectionFor:@"tableCell"];
|
|
567
|
+
auto number = [](NSDictionary *dict, NSString *key, CGFloat fallback) -> CGFloat {
|
|
568
|
+
NSNumber *value = [dict[key] isKindOfClass:[NSNumber class]] ? dict[key] : nil;
|
|
569
|
+
return value != nil ? value.doubleValue : fallback;
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
JMDLayoutStyle *rowDefaults = [JMDLayoutStyle
|
|
573
|
+
defaultsWithBackground:nil
|
|
574
|
+
padding:UIEdgeInsetsZero
|
|
575
|
+
borderRadius:0
|
|
576
|
+
borderLeftColor:nil
|
|
577
|
+
borderLeftWidth:0];
|
|
578
|
+
JMDLayoutStyle *rowBase =
|
|
579
|
+
[JMDLayoutStyle fromJson:[styles_ rawSectionFor:@"tableRow"] defaults:rowDefaults];
|
|
580
|
+
JMDLayoutStyle *headerRowStyle =
|
|
581
|
+
[JMDLayoutStyle fromJson:[styles_ rawSectionFor:@"tableHeaderRow"] defaults:rowBase];
|
|
582
|
+
JMDLayoutStyle *bodyRowStyle =
|
|
583
|
+
[JMDLayoutStyle fromJson:[styles_ rawSectionFor:@"tableBodyRow"] defaults:rowBase];
|
|
584
|
+
|
|
585
|
+
ResolvedAttrs cellAttrs;
|
|
586
|
+
cellAttrs.fontSize = 16 * fontScale_;
|
|
587
|
+
cellAttrs.color = UIColor.blackColor;
|
|
588
|
+
applyStyle(cellAttrs, [styles_ textStyleFor:@"base"], fontScale_);
|
|
589
|
+
applyStyle(cellAttrs, [styles_ textStyleFor:@"paragraph"], fontScale_);
|
|
590
|
+
for (JMDTextStyle *style in inherited) {
|
|
591
|
+
applyStyle(cellAttrs, style, fontScale_);
|
|
592
|
+
}
|
|
593
|
+
applyStyle(cellAttrs, [styles_ textStyleFor:@"tableCell"], fontScale_);
|
|
594
|
+
|
|
595
|
+
NSMutableArray<JMDTableRow *> *rows = [NSMutableArray new];
|
|
596
|
+
for (const Node *rowNode : node->children) {
|
|
597
|
+
if (rowNode->type != NodeType::TableRow) {
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
JMDTableRow *row = [JMDTableRow new];
|
|
601
|
+
row.isHeader = rowNode->level == 1;
|
|
602
|
+
NSMutableArray<NSAttributedString *> *cells = [NSMutableArray new];
|
|
603
|
+
for (const Node *cellNode : rowNode->children) {
|
|
604
|
+
if (cellNode->type != NodeType::TableCell) {
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
ResolvedAttrs attrs = cellAttrs;
|
|
608
|
+
if (row.isHeader) {
|
|
609
|
+
attrs.weight = 700;
|
|
610
|
+
applyStyle(attrs, [styles_ textStyleFor:@"tableCell"], fontScale_);
|
|
611
|
+
applyStyle(attrs, [styles_ textStyleFor:@"tableHeaderCell"], fontScale_);
|
|
612
|
+
}
|
|
613
|
+
applyCenteringOffset(attrs);
|
|
614
|
+
NSMutableAttributedString *cell = [NSMutableAttributedString new];
|
|
615
|
+
walk(cell, cellNode, attrs);
|
|
616
|
+
[cells addObject:cell];
|
|
617
|
+
}
|
|
618
|
+
row.cells = cells;
|
|
619
|
+
[rows addObject:row];
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
JMDBlock *block = [JMDBlock new];
|
|
623
|
+
block.kind = JMDBlockKindTable;
|
|
624
|
+
block.tableRows = rows;
|
|
625
|
+
block.layoutStyle = [JMDLayoutStyle fromJson:tableSection defaults:nil];
|
|
626
|
+
block.headerRowStyle = headerRowStyle;
|
|
627
|
+
block.bodyRowStyle = bodyRowStyle;
|
|
628
|
+
block.cellPadding = UIEdgeInsetsMake(
|
|
629
|
+
number(cellSection, @"paddingTop", 0),
|
|
630
|
+
number(cellSection, @"paddingLeft", 0),
|
|
631
|
+
number(cellSection, @"paddingBottom", 0),
|
|
632
|
+
number(cellSection, @"paddingRight", 0));
|
|
633
|
+
// Header cells fall back to the body cell padding key-by-key.
|
|
634
|
+
NSDictionary *headerCellSection = [styles_ rawSectionFor:@"tableHeaderCell"];
|
|
635
|
+
const auto headerNumber = [&](NSString *key) {
|
|
636
|
+
NSNumber *value = [headerCellSection[key] isKindOfClass:[NSNumber class]]
|
|
637
|
+
? headerCellSection[key]
|
|
638
|
+
: nil;
|
|
639
|
+
return value != nil ? value.doubleValue : number(cellSection, key, 0);
|
|
640
|
+
};
|
|
641
|
+
block.headerCellPadding = UIEdgeInsetsMake(
|
|
642
|
+
headerNumber(@"paddingTop"),
|
|
643
|
+
headerNumber(@"paddingLeft"),
|
|
644
|
+
headerNumber(@"paddingBottom"),
|
|
645
|
+
headerNumber(@"paddingRight"));
|
|
646
|
+
// Unstyled columns take their natural widths; defaultStyles provides the
|
|
647
|
+
// classic [44, 320] clamps.
|
|
648
|
+
block.minColumnWidth = number(tableSection, @"minColumnWidth", 0);
|
|
649
|
+
block.maxColumnWidth = number(tableSection, @"maxColumnWidth", 0);
|
|
650
|
+
return block;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
JMDBlock *textBlock(const Node *node, NSArray<JMDTextStyle *> *inherited) {
|
|
654
|
+
NSMutableAttributedString *output = [NSMutableAttributedString new];
|
|
655
|
+
|
|
656
|
+
ResolvedAttrs base;
|
|
657
|
+
base.fontSize = 16 * fontScale_;
|
|
658
|
+
base.color = UIColor.blackColor;
|
|
659
|
+
// Unstyled output is fully plain: heading sizes/weights come from the
|
|
660
|
+
// hN sections (defaultStyles on the JS side), not builtins.
|
|
661
|
+
if (node->type == NodeType::Heading) {
|
|
662
|
+
applyStyle(base, [styles_ textStyleFor:@"base"], fontScale_);
|
|
663
|
+
for (JMDTextStyle *style in inherited) {
|
|
664
|
+
applyStyle(base, style, fontScale_);
|
|
665
|
+
}
|
|
666
|
+
// Headings shed the inherited lineHeight like they shed paragraph
|
|
667
|
+
// styles: a body lineHeight would cap the taller heading's line box
|
|
668
|
+
// and clip its ascenders. hN.lineHeight still applies.
|
|
669
|
+
base.lineHeight = 0;
|
|
670
|
+
applyStyle(
|
|
671
|
+
base,
|
|
672
|
+
[styles_ textStyleFor:[NSString stringWithFormat:@"h%d", (int)node->level]],
|
|
673
|
+
fontScale_);
|
|
674
|
+
} else {
|
|
675
|
+
applyStyle(base, [styles_ textStyleFor:@"base"], fontScale_);
|
|
676
|
+
applyStyle(base, [styles_ textStyleFor:@"paragraph"], fontScale_);
|
|
677
|
+
for (JMDTextStyle *style in inherited) {
|
|
678
|
+
applyStyle(base, style, fontScale_);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
applyCenteringOffset(base);
|
|
683
|
+
walk(output, node, base);
|
|
684
|
+
|
|
685
|
+
NSDictionary *spoilerSection = [styles_ rawSectionFor:@"spoiler"];
|
|
686
|
+
NSNumber *spoilerRadius =
|
|
687
|
+
[spoilerSection[@"borderRadius"] isKindOfClass:[NSNumber class]]
|
|
688
|
+
? spoilerSection[@"borderRadius"]
|
|
689
|
+
: @0;
|
|
690
|
+
|
|
691
|
+
JMDBlock *block = [JMDBlock new];
|
|
692
|
+
block.kind = JMDBlockKindText;
|
|
693
|
+
block.attributedText = output;
|
|
694
|
+
// Neutral functional floor — the cover must hide text even unstyled;
|
|
695
|
+
// defaultStyles provides the styled cover.
|
|
696
|
+
block.spoilerColor = [JMDTextStyle colorFromJson:spoilerSection[@"backgroundColor"]]
|
|
697
|
+
?: UIColor.blackColor;
|
|
698
|
+
block.spoilerRadius = spoilerRadius.doubleValue;
|
|
699
|
+
block.spoilerContinuous =
|
|
700
|
+
[spoilerSection[@"borderCurve"] isEqual:@"continuous"];
|
|
701
|
+
return block;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
private:
|
|
705
|
+
static NSArray<JMDTextStyle *> *appendStyle(
|
|
706
|
+
NSArray<JMDTextStyle *> *chain, JMDTextStyle *style) {
|
|
707
|
+
if (style == nil) {
|
|
708
|
+
return chain;
|
|
709
|
+
}
|
|
710
|
+
return [chain arrayByAddingObject:style];
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
void append(NSMutableAttributedString *output, NSString *text, const ResolvedAttrs &attrs) {
|
|
714
|
+
if (text.length == 0) {
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
[output appendAttributedString:[[NSAttributedString alloc]
|
|
718
|
+
initWithString:text
|
|
719
|
+
attributes:attributesDictionary(attrs)]];
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
void walk(NSMutableAttributedString *output, const Node *parent, const ResolvedAttrs &attrs) {
|
|
723
|
+
for (const Node *node : parent->children) {
|
|
724
|
+
switch (node->type) {
|
|
725
|
+
case NodeType::Text:
|
|
726
|
+
append(output, toNSString(node->text), attrs);
|
|
727
|
+
break;
|
|
728
|
+
case NodeType::SoftBreak:
|
|
729
|
+
append(output, @" ", attrs);
|
|
730
|
+
break;
|
|
731
|
+
case NodeType::HardBreak:
|
|
732
|
+
append(output, @"\n", attrs);
|
|
733
|
+
break;
|
|
734
|
+
case NodeType::Bold: {
|
|
735
|
+
ResolvedAttrs next = attrs;
|
|
736
|
+
next.weight = 700;
|
|
737
|
+
applyStyle(next, [styles_ textStyleFor:@"bold"], fontScale_);
|
|
738
|
+
walk(output, node, next);
|
|
739
|
+
break;
|
|
740
|
+
}
|
|
741
|
+
case NodeType::Italic: {
|
|
742
|
+
ResolvedAttrs next = attrs;
|
|
743
|
+
next.italic = true;
|
|
744
|
+
applyStyle(next, [styles_ textStyleFor:@"italic"], fontScale_);
|
|
745
|
+
walk(output, node, next);
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
748
|
+
case NodeType::Strikethrough: {
|
|
749
|
+
ResolvedAttrs next = attrs;
|
|
750
|
+
next.strikethrough = true;
|
|
751
|
+
applyStyle(next, [styles_ textStyleFor:@"strikethrough"], fontScale_);
|
|
752
|
+
walk(output, node, next);
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
case NodeType::Link: {
|
|
756
|
+
ResolvedAttrs next = attrs;
|
|
757
|
+
NSString *url = toNSString(node->url);
|
|
758
|
+
next.linkUrl = url;
|
|
759
|
+
JMDTextStyle *variantStyle = nil;
|
|
760
|
+
bool isMention = false;
|
|
761
|
+
for (JMDMentionVariant *variant in styles_.mentionVariants) {
|
|
762
|
+
if ([variant.pattern firstMatchInString:url
|
|
763
|
+
options:0
|
|
764
|
+
range:NSMakeRange(0, url.length)] != nil) {
|
|
765
|
+
isMention = true;
|
|
766
|
+
variantStyle = variant.style;
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (isMention) {
|
|
771
|
+
applyStyle(next, [styles_ textStyleFor:@"mention"], fontScale_);
|
|
772
|
+
applyStyle(next, variantStyle, fontScale_);
|
|
773
|
+
applyChipStyle(next, [styles_ rawSectionFor:@"mention"]);
|
|
774
|
+
} else {
|
|
775
|
+
applyStyle(next, [styles_ textStyleFor:@"link"], fontScale_);
|
|
776
|
+
applyChipStyle(next, [styles_ rawSectionFor:@"link"]);
|
|
777
|
+
}
|
|
778
|
+
if (next.backgroundColor != nil) {
|
|
779
|
+
JMDRunBackground *chip = [JMDRunBackground new];
|
|
780
|
+
chip.color = next.backgroundColor;
|
|
781
|
+
chip.radius = next.chipRadius;
|
|
782
|
+
chip.continuousCurve = next.chipContinuous;
|
|
783
|
+
chip.padLeft = next.chipPadLeft;
|
|
784
|
+
chip.padRight = next.chipPadRight;
|
|
785
|
+
next.chipInstance = chip;
|
|
786
|
+
}
|
|
787
|
+
walk(output, node, next);
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
case NodeType::InlineCode: {
|
|
791
|
+
ResolvedAttrs next = attrs;
|
|
792
|
+
next.family = @"Menlo";
|
|
793
|
+
applyStyle(next, [styles_ textStyleFor:@"inlineCode"], fontScale_);
|
|
794
|
+
applyChipStyle(next, [styles_ rawSectionFor:@"inlineCode"]);
|
|
795
|
+
append(output, toNSString(node->text), next);
|
|
796
|
+
break;
|
|
797
|
+
}
|
|
798
|
+
case NodeType::Superscript: {
|
|
799
|
+
ResolvedAttrs next = attrs;
|
|
800
|
+
next.fontSize = attrs.fontSize * 0.7;
|
|
801
|
+
next.baselineOffset = attrs.fontSize * 0.35;
|
|
802
|
+
applyStyle(next, [styles_ textStyleFor:@"superscript"], fontScale_);
|
|
803
|
+
walk(output, node, next);
|
|
804
|
+
break;
|
|
805
|
+
}
|
|
806
|
+
case NodeType::Subscript: {
|
|
807
|
+
ResolvedAttrs next = attrs;
|
|
808
|
+
next.fontSize = attrs.fontSize * 0.7;
|
|
809
|
+
next.baselineOffset = -attrs.fontSize * 0.18;
|
|
810
|
+
applyStyle(next, [styles_ textStyleFor:@"subscript"], fontScale_);
|
|
811
|
+
walk(output, node, next);
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
case NodeType::Spoiler: {
|
|
815
|
+
ResolvedAttrs next = attrs;
|
|
816
|
+
next.spoilerId = spoilerCounter_++;
|
|
817
|
+
walk(output, node, next);
|
|
818
|
+
break;
|
|
819
|
+
}
|
|
820
|
+
case NodeType::Image:
|
|
821
|
+
append(output, toNSString(node->text), attrs);
|
|
822
|
+
break;
|
|
823
|
+
default:
|
|
824
|
+
walk(output, node, attrs);
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
JMDStyleConfig *styles_;
|
|
831
|
+
CGFloat fontScale_;
|
|
832
|
+
NSInteger spoilerCounter_ = 0;
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
} // namespace
|
|
836
|
+
|
|
837
|
+
@implementation JMDBlockRenderer
|
|
838
|
+
|
|
839
|
+
+ (NSArray<JMDBlock *> *)renderMarkdown:(NSString *)markdown
|
|
840
|
+
styles:(JMDStyleConfig *)styles
|
|
841
|
+
fontScale:(CGFloat)fontScale {
|
|
842
|
+
const auto document =
|
|
843
|
+
jetmarkdown::parseMarkdown(std::string([markdown UTF8String] ?: ""));
|
|
844
|
+
|
|
845
|
+
BlockBuilder builder(styles, fontScale);
|
|
846
|
+
if (document->root == nullptr) {
|
|
847
|
+
return @[];
|
|
848
|
+
}
|
|
849
|
+
return builder.renderBlocks(document->root->children, @[]);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
@end
|