react-native-jet-markdown 0.2.1 → 0.2.2
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/README.md +4 -56
- package/android/src/main/cpp/JniBindings.cpp +0 -106
- package/android/src/main/java/com/jetmarkdown/JetMarkdownNative.kt +0 -79
- package/android/src/main/java/com/jetmarkdown/JetMarkdownPackage.kt +1 -2
- package/android/src/main/java/com/jetmarkdown/render/spans/MarkdownLineHeightSpan.kt +2 -13
- package/cpp/react/override/react/renderer/components/JetMarkdownViewSpec/ComponentDescriptors.h +0 -4
- package/cpp/tests/parser_tests.cpp +0 -402
- package/cpp/tests/run_tests.sh +0 -3
- package/lib/module/index.js +0 -2
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +0 -127
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +2 -5
- package/src/index.tsx +0 -9
- package/src/types.ts +0 -134
- package/android/src/main/java/com/jetmarkdown/editor/EditorSpans.kt +0 -217
- package/android/src/main/java/com/jetmarkdown/editor/JetMarkdownEditorManager.kt +0 -224
- package/android/src/main/java/com/jetmarkdown/editor/JetMarkdownEditorView.kt +0 -1736
- package/cpp/core/AstToMarkdown.cpp +0 -549
- package/cpp/core/AstToMarkdown.h +0 -16
- package/cpp/core/EditorRuns.cpp +0 -640
- package/cpp/core/EditorRuns.h +0 -94
- package/cpp/core/EditorText.cpp +0 -15
- package/cpp/core/EditorText.h +0 -18
- package/cpp/react/JetMarkdownEditorShadowNode.cpp +0 -54
- package/cpp/react/JetMarkdownEditorShadowNode.h +0 -55
- package/cpp/react/JetMarkdownEditorState.h +0 -35
- package/ios/editor/JetMarkdownEditor.h +0 -12
- package/ios/editor/JetMarkdownEditor.mm +0 -2002
- package/lib/module/JetMarkdownEditor.js +0 -85
- package/lib/module/JetMarkdownEditor.js.map +0 -1
- package/lib/module/JetMarkdownEditor.native.js +0 -244
- package/lib/module/JetMarkdownEditor.native.js.map +0 -1
- package/lib/module/JetMarkdownEditorNativeComponent.ts +0 -173
- package/lib/module/useJetMarkdownEditor.js +0 -41
- package/lib/module/useJetMarkdownEditor.js.map +0 -1
- package/lib/typescript/src/JetMarkdownEditor.d.ts +0 -6
- package/lib/typescript/src/JetMarkdownEditor.d.ts.map +0 -1
- package/lib/typescript/src/JetMarkdownEditor.native.d.ts +0 -6
- package/lib/typescript/src/JetMarkdownEditor.native.d.ts.map +0 -1
- package/lib/typescript/src/JetMarkdownEditorNativeComponent.d.ts +0 -105
- package/lib/typescript/src/JetMarkdownEditorNativeComponent.d.ts.map +0 -1
- package/lib/typescript/src/useJetMarkdownEditor.d.ts +0 -36
- package/lib/typescript/src/useJetMarkdownEditor.d.ts.map +0 -1
- package/src/JetMarkdownEditor.native.tsx +0 -301
- package/src/JetMarkdownEditor.tsx +0 -90
- package/src/JetMarkdownEditorNativeComponent.ts +0 -173
- package/src/useJetMarkdownEditor.ts +0 -50
|
@@ -1,2002 +0,0 @@
|
|
|
1
|
-
#import "JetMarkdownEditor.h"
|
|
2
|
-
|
|
3
|
-
#import <React/RCTConversions.h>
|
|
4
|
-
#import <react/renderer/components/JetMarkdownViewSpec/EventEmitters.h>
|
|
5
|
-
#import <react/renderer/components/JetMarkdownViewSpec/Props.h>
|
|
6
|
-
#import <react/renderer/components/JetMarkdownViewSpec/RCTComponentViewHelpers.h>
|
|
7
|
-
#import <react/renderer/core/ConcreteComponentDescriptor.h>
|
|
8
|
-
|
|
9
|
-
#import <vector>
|
|
10
|
-
|
|
11
|
-
#import "../../cpp/core/EditorRuns.h"
|
|
12
|
-
#import "../../cpp/react/JetMarkdownEditorShadowNode.h"
|
|
13
|
-
#import "../style/JMDFontScale.h"
|
|
14
|
-
#import "../style/JMDStyleConfig.h"
|
|
15
|
-
#import "../style/JMDTextStyle.h"
|
|
16
|
-
|
|
17
|
-
using namespace facebook::react;
|
|
18
|
-
|
|
19
|
-
// Imported directly (like the viewer) so the descriptor binds to the custom
|
|
20
|
-
// measurable shadow node regardless of include order.
|
|
21
|
-
using JMDEditorComponentDescriptor =
|
|
22
|
-
ConcreteComponentDescriptor<JetMarkdownEditorShadowNode>;
|
|
23
|
-
|
|
24
|
-
// Source of truth for inline marks: a bitmask of jetmarkdown::EditorMark
|
|
25
|
-
// stored as a custom attribute. Display attributes (fonts, strikethrough,
|
|
26
|
-
// backgrounds) are always derived from it.
|
|
27
|
-
static NSAttributedStringKey const JMDEditorMarksAttribute = @"JMDEditorMarks";
|
|
28
|
-
|
|
29
|
-
// Source of truth for the line's block type: (EditorBlockType << 8) | level,
|
|
30
|
-
// stored on every character of the line (and carried by the newline).
|
|
31
|
-
static NSAttributedStringKey const JMDEditorBlockAttribute = @"JMDEditorBlock";
|
|
32
|
-
|
|
33
|
-
// Linked ranges: the URL string. Mentions are links with app-scheme URLs
|
|
34
|
-
// plus the atomic flag (the token edits as one unit).
|
|
35
|
-
static NSAttributedStringKey const JMDEditorLinkAttribute = @"JMDEditorLink";
|
|
36
|
-
static NSAttributedStringKey const JMDEditorAtomicAttribute = @"JMDEditorAtomic";
|
|
37
|
-
|
|
38
|
-
static NSString *JMDStringFromCpp(const std::string &value) {
|
|
39
|
-
return [[NSString alloc] initWithBytes:value.data()
|
|
40
|
-
length:value.size()
|
|
41
|
-
encoding:NSUTF8StringEncoding]
|
|
42
|
-
?: @"";
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
static uint32_t JMDFlagsFromValue(id value) {
|
|
46
|
-
return value == nil ? 0 : [(NSNumber *)value unsignedIntValue];
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
static uint32_t JMDPackBlock(jetmarkdown::EditorBlockType type, uint8_t level) {
|
|
50
|
-
return (static_cast<uint32_t>(type) << 8) | level;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
static jetmarkdown::EditorBlockType JMDBlockType(uint32_t packed) {
|
|
54
|
-
return static_cast<jetmarkdown::EditorBlockType>(packed >> 8);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
static BOOL JMDBlockIsList(uint32_t packed) {
|
|
58
|
-
const auto type = JMDBlockType(packed);
|
|
59
|
-
return type == jetmarkdown::EditorBlockType::Bullet ||
|
|
60
|
-
type == jetmarkdown::EditorBlockType::Ordered;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
@class JetMarkdownEditor;
|
|
64
|
-
|
|
65
|
-
// Draws list markers and quote bars in the gutter created by the line
|
|
66
|
-
// blocks' paragraph indents. Sits above the text view; never interactive.
|
|
67
|
-
@interface JMDEditorMarkerView : UIView
|
|
68
|
-
@property (nonatomic, weak) JetMarkdownEditor *editor;
|
|
69
|
-
@end
|
|
70
|
-
|
|
71
|
-
// Draws the full-width background stripe behind code-block lines. Sits
|
|
72
|
-
// below the text view so glyphs stay crisp.
|
|
73
|
-
@interface JMDEditorCodeBackgroundView : UIView
|
|
74
|
-
@property (nonatomic, weak) JetMarkdownEditor *editor;
|
|
75
|
-
@end
|
|
76
|
-
|
|
77
|
-
@protocol JMDEditorTextViewActions <NSObject>
|
|
78
|
-
- (void)editorTextViewDidPaste;
|
|
79
|
-
- (void)editorTextViewShortcut:(uint32_t)mark;
|
|
80
|
-
// Backspace with the caret at the very start of the document: UIKit has
|
|
81
|
-
// nothing to delete, but a formatted first line should shed its block.
|
|
82
|
-
- (BOOL)editorTextViewHandleDeleteAtDocumentStart;
|
|
83
|
-
@end
|
|
84
|
-
|
|
85
|
-
// Intercepts paste (the clipboard is reported to JS, which owns the
|
|
86
|
-
// default insertion) and adds hardware keyboard formatting shortcuts.
|
|
87
|
-
@interface JMDEditorTextView : UITextView
|
|
88
|
-
@property (nonatomic, weak) id<JMDEditorTextViewActions> actionDelegate;
|
|
89
|
-
@end
|
|
90
|
-
|
|
91
|
-
@implementation JMDEditorTextView
|
|
92
|
-
|
|
93
|
-
- (void)paste:(id)sender {
|
|
94
|
-
[self.actionDelegate editorTextViewDidPaste];
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
- (void)deleteBackward {
|
|
98
|
-
if (self.selectedRange.location == 0 && self.selectedRange.length == 0 &&
|
|
99
|
-
[self.actionDelegate editorTextViewHandleDeleteAtDocumentStart]) {
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
[super deleteBackward];
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
- (NSArray<UIKeyCommand *> *)keyCommands {
|
|
106
|
-
UIKeyCommand *bold = [UIKeyCommand keyCommandWithInput:@"b"
|
|
107
|
-
modifierFlags:UIKeyModifierCommand
|
|
108
|
-
action:@selector(jmdToggleBold:)];
|
|
109
|
-
UIKeyCommand *italic = [UIKeyCommand keyCommandWithInput:@"i"
|
|
110
|
-
modifierFlags:UIKeyModifierCommand
|
|
111
|
-
action:@selector(jmdToggleItalic:)];
|
|
112
|
-
UIKeyCommand *strike = [UIKeyCommand
|
|
113
|
-
keyCommandWithInput:@"x"
|
|
114
|
-
modifierFlags:UIKeyModifierCommand | UIKeyModifierShift
|
|
115
|
-
action:@selector(jmdToggleStrikethrough:)];
|
|
116
|
-
if (@available(iOS 15.0, *)) {
|
|
117
|
-
bold.wantsPriorityOverSystemBehavior = YES;
|
|
118
|
-
italic.wantsPriorityOverSystemBehavior = YES;
|
|
119
|
-
strike.wantsPriorityOverSystemBehavior = YES;
|
|
120
|
-
}
|
|
121
|
-
return @[ bold, italic, strike ];
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
- (void)jmdToggleBold:(UIKeyCommand *)command {
|
|
125
|
-
[self.actionDelegate editorTextViewShortcut:jetmarkdown::MarkBold];
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
- (void)jmdToggleItalic:(UIKeyCommand *)command {
|
|
129
|
-
[self.actionDelegate editorTextViewShortcut:jetmarkdown::MarkItalic];
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
- (void)jmdToggleStrikethrough:(UIKeyCommand *)command {
|
|
133
|
-
[self.actionDelegate editorTextViewShortcut:jetmarkdown::MarkStrikethrough];
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
@end
|
|
137
|
-
|
|
138
|
-
@interface JetMarkdownEditor () <UITextViewDelegate,
|
|
139
|
-
JMDEditorTextViewActions,
|
|
140
|
-
RCTJetMarkdownEditorViewProtocol>
|
|
141
|
-
- (void)drawMarkersInContext:(CGContextRef)context view:(JMDEditorMarkerView *)view;
|
|
142
|
-
- (void)drawCodeBackgroundsInView:(JMDEditorCodeBackgroundView *)view;
|
|
143
|
-
@end
|
|
144
|
-
|
|
145
|
-
@implementation JMDEditorMarkerView
|
|
146
|
-
|
|
147
|
-
- (instancetype)initWithFrame:(CGRect)frame {
|
|
148
|
-
if (self = [super initWithFrame:frame]) {
|
|
149
|
-
self.userInteractionEnabled = NO;
|
|
150
|
-
self.backgroundColor = UIColor.clearColor;
|
|
151
|
-
self.contentMode = UIViewContentModeRedraw;
|
|
152
|
-
}
|
|
153
|
-
return self;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
- (void)drawRect:(CGRect)rect {
|
|
157
|
-
[self.editor drawMarkersInContext:UIGraphicsGetCurrentContext() view:self];
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
@end
|
|
161
|
-
|
|
162
|
-
@implementation JMDEditorCodeBackgroundView
|
|
163
|
-
|
|
164
|
-
- (instancetype)initWithFrame:(CGRect)frame {
|
|
165
|
-
if (self = [super initWithFrame:frame]) {
|
|
166
|
-
self.userInteractionEnabled = NO;
|
|
167
|
-
self.backgroundColor = UIColor.clearColor;
|
|
168
|
-
self.contentMode = UIViewContentModeRedraw;
|
|
169
|
-
}
|
|
170
|
-
return self;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
- (void)drawRect:(CGRect)rect {
|
|
174
|
-
[self.editor drawCodeBackgroundsInView:self];
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
@end
|
|
178
|
-
|
|
179
|
-
@implementation JetMarkdownEditor {
|
|
180
|
-
JMDEditorTextView *_textView;
|
|
181
|
-
UILabel *_placeholderLabel;
|
|
182
|
-
JMDEditorMarkerView *_markerView;
|
|
183
|
-
JMDEditorCodeBackgroundView *_codeBackgroundView;
|
|
184
|
-
NSString *_stylesJson;
|
|
185
|
-
BOOL _defaultValueApplied;
|
|
186
|
-
BOOL _autoFocusHandled;
|
|
187
|
-
BOOL _multiline;
|
|
188
|
-
BOOL _propScrollEnabled;
|
|
189
|
-
// Autogrow cap; 0 = unbounded. Past it the text view scrolls internally.
|
|
190
|
-
CGFloat _maxHeight;
|
|
191
|
-
CGFloat _lastPublishedHeight;
|
|
192
|
-
UIFont *_baseFont;
|
|
193
|
-
UIColor *_baseColor;
|
|
194
|
-
// Resolved lineHeight per context; 0 = natural. Headings and code use
|
|
195
|
-
// their own element style, everything else the base/paragraph cascade.
|
|
196
|
-
CGFloat _lineHeight;
|
|
197
|
-
CGFloat _headingLineHeights[7];
|
|
198
|
-
CGFloat _codeLineHeight;
|
|
199
|
-
// Marks armed for text typed at the collapsed cursor. Explicit while the
|
|
200
|
-
// user has toggled at this caret position; re-derived from the character
|
|
201
|
-
// before the caret whenever the selection moves.
|
|
202
|
-
uint32_t _typingFlags;
|
|
203
|
-
// Block armed for the caret's line (empty lines carry no characters, so
|
|
204
|
-
// the attribute alone cannot represent them).
|
|
205
|
-
uint32_t _typingBlock;
|
|
206
|
-
BOOL _paragraphAfterNewline;
|
|
207
|
-
// Autocorrect/QuickType replace a whole word ("Ab" → "An"), rebuilding it
|
|
208
|
-
// with attributes that drop the custom mark keys. The replaced range's
|
|
209
|
-
// per-character flags are captured in shouldChangeTextInRange and
|
|
210
|
-
// restored onto the committed text in textViewDidChange.
|
|
211
|
-
std::vector<uint32_t> _replacedCharFlags;
|
|
212
|
-
NSRange _markRestoreRange;
|
|
213
|
-
BOOL _pendingMarkRestore;
|
|
214
|
-
BOOL _markdownEmitScheduled;
|
|
215
|
-
// Autocorrect/autocapitalize from props; suppressed while the caret is in
|
|
216
|
-
// a code context (code block line or armed inline-code mark).
|
|
217
|
-
BOOL _propAutoCorrect;
|
|
218
|
-
UITextAutocapitalizationType _propAutoCapitalize;
|
|
219
|
-
BOOL _allowFontScaling;
|
|
220
|
-
// Dynamic Type multiplier applied to font sizes and line heights; 1 when
|
|
221
|
-
// allowFontScaling is off. Must match the shadow node's fontSizeMultiplier.
|
|
222
|
-
CGFloat _fontScale;
|
|
223
|
-
BOOL _suppressFocusEvents;
|
|
224
|
-
UIColor *_linkColor;
|
|
225
|
-
NSArray<NSString *> *_mentionTriggers;
|
|
226
|
-
BOOL _mentionActive;
|
|
227
|
-
NSString *_mentionTrigger;
|
|
228
|
-
NSUInteger _mentionStart;
|
|
229
|
-
// Dedupes onMentionChange: typing fires both didChangeSelection and
|
|
230
|
-
// didChange, which each re-evaluate the session.
|
|
231
|
-
NSString *_lastMentionQuery;
|
|
232
|
-
NSRange _lastSelection;
|
|
233
|
-
uint64_t _lastStateKey;
|
|
234
|
-
BOOL _stateEmitted;
|
|
235
|
-
JetMarkdownEditorShadowNode::ConcreteState::Shared _state;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
+ (ComponentDescriptorProvider)componentDescriptorProvider {
|
|
239
|
-
return concreteComponentDescriptorProvider<JMDEditorComponentDescriptor>();
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
- (instancetype)initWithFrame:(CGRect)frame {
|
|
243
|
-
if (self = [super initWithFrame:frame]) {
|
|
244
|
-
static const auto defaultProps = std::make_shared<const JetMarkdownEditorProps>();
|
|
245
|
-
_props = defaultProps;
|
|
246
|
-
_stylesJson = @"";
|
|
247
|
-
_multiline = YES;
|
|
248
|
-
_allowFontScaling = YES;
|
|
249
|
-
_fontScale = JMDFontSizeMultiplier();
|
|
250
|
-
|
|
251
|
-
[NSNotificationCenter.defaultCenter
|
|
252
|
-
addObserver:self
|
|
253
|
-
selector:@selector(jmdContentSizeCategoryDidChange)
|
|
254
|
-
name:UIContentSizeCategoryDidChangeNotification
|
|
255
|
-
object:nil];
|
|
256
|
-
_lastPublishedHeight = 0;
|
|
257
|
-
_baseFont = [UIFont systemFontOfSize:16];
|
|
258
|
-
_baseColor = UIColor.blackColor;
|
|
259
|
-
_linkColor = UIColor.systemBlueColor;
|
|
260
|
-
_mentionTriggers = @[];
|
|
261
|
-
_lastSelection = NSMakeRange(0, 0);
|
|
262
|
-
|
|
263
|
-
_codeBackgroundView = [[JMDEditorCodeBackgroundView alloc] initWithFrame:CGRectZero];
|
|
264
|
-
_codeBackgroundView.editor = self;
|
|
265
|
-
[self addSubview:_codeBackgroundView];
|
|
266
|
-
|
|
267
|
-
_textView = [[JMDEditorTextView alloc] initWithFrame:CGRectZero];
|
|
268
|
-
_textView.actionDelegate = self;
|
|
269
|
-
_textView.backgroundColor = UIColor.clearColor;
|
|
270
|
-
_textView.delegate = self;
|
|
271
|
-
_textView.scrollEnabled = NO;
|
|
272
|
-
_textView.textContainer.lineFragmentPadding = 0;
|
|
273
|
-
_textView.textContainerInset = UIEdgeInsetsZero;
|
|
274
|
-
[self addSubview:_textView];
|
|
275
|
-
|
|
276
|
-
_markerView = [[JMDEditorMarkerView alloc] initWithFrame:CGRectZero];
|
|
277
|
-
_markerView.editor = self;
|
|
278
|
-
[self addSubview:_markerView];
|
|
279
|
-
|
|
280
|
-
_placeholderLabel = [[UILabel alloc] initWithFrame:CGRectZero];
|
|
281
|
-
_placeholderLabel.numberOfLines = 1;
|
|
282
|
-
_placeholderLabel.userInteractionEnabled = NO;
|
|
283
|
-
[self addSubview:_placeholderLabel];
|
|
284
|
-
|
|
285
|
-
[self applyTextStyles];
|
|
286
|
-
}
|
|
287
|
-
return self;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
#pragma mark - Styles
|
|
291
|
-
|
|
292
|
-
// Root text attributes come from the same cascade the viewer uses:
|
|
293
|
-
// base (style prop text keys) then paragraph, floored at 16pt black.
|
|
294
|
-
- (void)applyTextStyles {
|
|
295
|
-
JMDStyleConfig *styles = [JMDStyleConfig configWithJson:_stylesJson];
|
|
296
|
-
|
|
297
|
-
CGFloat fontSize = 16;
|
|
298
|
-
NSString *fontFamily = nil;
|
|
299
|
-
UIColor *color = UIColor.blackColor;
|
|
300
|
-
CGFloat lineHeight = 0;
|
|
301
|
-
for (NSString *key in @[ @"base", @"paragraph" ]) {
|
|
302
|
-
JMDTextStyle *style = [styles textStyleFor:key];
|
|
303
|
-
if (style.fontSize != nil) {
|
|
304
|
-
fontSize = style.fontSize.doubleValue;
|
|
305
|
-
}
|
|
306
|
-
if (style.fontFamily != nil) {
|
|
307
|
-
fontFamily = style.fontFamily;
|
|
308
|
-
}
|
|
309
|
-
if (style.color != nil) {
|
|
310
|
-
color = style.color;
|
|
311
|
-
}
|
|
312
|
-
if (style.lineHeight != nil) {
|
|
313
|
-
lineHeight = style.lineHeight.doubleValue;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
_fontScale = _allowFontScaling ? JMDFontSizeMultiplier() : 1.0;
|
|
317
|
-
fontSize *= _fontScale;
|
|
318
|
-
lineHeight *= _fontScale;
|
|
319
|
-
|
|
320
|
-
_lineHeight = lineHeight;
|
|
321
|
-
for (uint8_t level = 1; level <= 6; level++) {
|
|
322
|
-
JMDTextStyle *heading =
|
|
323
|
-
[styles textStyleFor:[NSString stringWithFormat:@"h%d", level]];
|
|
324
|
-
_headingLineHeights[level] =
|
|
325
|
-
(heading.lineHeight != nil ? heading.lineHeight.doubleValue : 0) *
|
|
326
|
-
_fontScale;
|
|
327
|
-
}
|
|
328
|
-
JMDTextStyle *codeStyle = [styles textStyleFor:@"codeBlock"];
|
|
329
|
-
_codeLineHeight = codeStyle.lineHeight != nil
|
|
330
|
-
? codeStyle.lineHeight.doubleValue * _fontScale
|
|
331
|
-
: lineHeight;
|
|
332
|
-
|
|
333
|
-
UIFont *font = nil;
|
|
334
|
-
if (fontFamily != nil) {
|
|
335
|
-
font = [UIFont fontWithName:fontFamily size:fontSize];
|
|
336
|
-
}
|
|
337
|
-
if (font == nil) {
|
|
338
|
-
font = [UIFont systemFontOfSize:fontSize];
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
_baseFont = font;
|
|
342
|
-
_baseColor = color;
|
|
343
|
-
_linkColor = [styles textStyleFor:@"link"].color ?: UIColor.systemBlueColor;
|
|
344
|
-
_textView.font = font;
|
|
345
|
-
_textView.textColor = color;
|
|
346
|
-
_textView.textContainerInset = UIEdgeInsetsMake(
|
|
347
|
-
styles.paddingTop, styles.paddingLeft, styles.paddingBottom, styles.paddingRight);
|
|
348
|
-
self.backgroundColor = styles.backgroundColor ?: UIColor.clearColor;
|
|
349
|
-
|
|
350
|
-
[self refreshDisplayAttributesInRange:NSMakeRange(0, _textView.textStorage.length)];
|
|
351
|
-
[self applyTypingAttributes];
|
|
352
|
-
|
|
353
|
-
_placeholderLabel.font = font;
|
|
354
|
-
[self setNeedsLayout];
|
|
355
|
-
[self invalidateDecorations];
|
|
356
|
-
[self publishHeight];
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
#pragma mark - Attributes
|
|
360
|
-
|
|
361
|
-
- (NSDictionary<NSAttributedStringKey, id> *)attributesForFlags:(uint32_t)flags
|
|
362
|
-
block:(uint32_t)block {
|
|
363
|
-
return [self attributesForFlags:flags block:block link:nil atomic:NO];
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
- (NSDictionary<NSAttributedStringKey, id> *)attributesForFlags:(uint32_t)flags
|
|
367
|
-
block:(uint32_t)block
|
|
368
|
-
link:(NSString *)link
|
|
369
|
-
atomic:(BOOL)atomic {
|
|
370
|
-
const auto blockType = JMDBlockType(block);
|
|
371
|
-
const uint8_t level = block & 0xFF;
|
|
372
|
-
const BOOL isCodeBlock = blockType == jetmarkdown::EditorBlockType::Code;
|
|
373
|
-
const BOOL isHeading = blockType == jetmarkdown::EditorBlockType::Heading;
|
|
374
|
-
const BOOL isCode = isCodeBlock || (flags & jetmarkdown::MarkInlineCode) != 0;
|
|
375
|
-
const BOOL isSuper = (flags & jetmarkdown::MarkSuperscript) != 0;
|
|
376
|
-
const BOOL isSub = (flags & jetmarkdown::MarkSubscript) != 0;
|
|
377
|
-
|
|
378
|
-
static const CGFloat headingScale[7] = {1, 2.0, 1.5, 1.25, 1.125, 1.0, 0.875};
|
|
379
|
-
CGFloat size = _baseFont.pointSize;
|
|
380
|
-
if (isHeading) {
|
|
381
|
-
size *= headingScale[MIN(level, (uint8_t)6)];
|
|
382
|
-
}
|
|
383
|
-
// Sup/sub match the viewer's 0.7 scaling.
|
|
384
|
-
if (isSuper || isSub) {
|
|
385
|
-
size *= 0.7;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
UIFont *font = isCode
|
|
389
|
-
? [UIFont monospacedSystemFontOfSize:size weight:UIFontWeightRegular]
|
|
390
|
-
: [_baseFont fontWithSize:size];
|
|
391
|
-
|
|
392
|
-
UIFontDescriptorSymbolicTraits traits = font.fontDescriptor.symbolicTraits;
|
|
393
|
-
if ((flags & jetmarkdown::MarkBold) != 0 || isHeading) {
|
|
394
|
-
traits |= UIFontDescriptorTraitBold;
|
|
395
|
-
}
|
|
396
|
-
if ((flags & jetmarkdown::MarkItalic) != 0) {
|
|
397
|
-
traits |= UIFontDescriptorTraitItalic;
|
|
398
|
-
}
|
|
399
|
-
UIFontDescriptor *descriptor =
|
|
400
|
-
[font.fontDescriptor fontDescriptorWithSymbolicTraits:traits];
|
|
401
|
-
if (descriptor != nil) {
|
|
402
|
-
font = [UIFont fontWithDescriptor:descriptor size:size];
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
NSMutableDictionary<NSAttributedStringKey, id> *attributes =
|
|
406
|
-
[NSMutableDictionary dictionary];
|
|
407
|
-
attributes[NSFontAttributeName] = font;
|
|
408
|
-
attributes[NSForegroundColorAttributeName] = _baseColor;
|
|
409
|
-
if ((flags & jetmarkdown::MarkStrikethrough) != 0) {
|
|
410
|
-
attributes[NSStrikethroughStyleAttributeName] = @(NSUnderlineStyleSingle);
|
|
411
|
-
}
|
|
412
|
-
// Inline code gets a per-run background; code BLOCK lines get a
|
|
413
|
-
// full-width stripe from the background view instead.
|
|
414
|
-
if ((flags & jetmarkdown::MarkInlineCode) != 0) {
|
|
415
|
-
attributes[NSBackgroundColorAttributeName] =
|
|
416
|
-
[UIColor colorWithWhite:0.5 alpha:0.15];
|
|
417
|
-
}
|
|
418
|
-
if ((flags & jetmarkdown::MarkSpoiler) != 0) {
|
|
419
|
-
attributes[NSBackgroundColorAttributeName] =
|
|
420
|
-
[UIColor colorWithWhite:0.35 alpha:0.25];
|
|
421
|
-
}
|
|
422
|
-
CGFloat baselineOffset = 0;
|
|
423
|
-
if (isSuper) {
|
|
424
|
-
baselineOffset = _baseFont.pointSize * 0.33;
|
|
425
|
-
} else if (isSub) {
|
|
426
|
-
baselineOffset = -_baseFont.pointSize * 0.15;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// Line height: headings/code use their element style, everything else
|
|
430
|
-
// the base/paragraph cascade (0 = natural). Glyphs center in the line
|
|
431
|
-
// box, matching React Native.
|
|
432
|
-
CGFloat lineHeight = _lineHeight;
|
|
433
|
-
if (isHeading) {
|
|
434
|
-
lineHeight = _headingLineHeights[MIN(level, (uint8_t)6)];
|
|
435
|
-
} else if (isCodeBlock) {
|
|
436
|
-
lineHeight = _codeLineHeight;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
NSMutableParagraphStyle *paragraph = [self paragraphStyleForBlock:block];
|
|
440
|
-
if (lineHeight > 0) {
|
|
441
|
-
if (paragraph == nil) {
|
|
442
|
-
paragraph = [[NSMutableParagraphStyle alloc] init];
|
|
443
|
-
}
|
|
444
|
-
paragraph.minimumLineHeight = lineHeight;
|
|
445
|
-
paragraph.maximumLineHeight = lineHeight;
|
|
446
|
-
const CGFloat delta = lineHeight - font.lineHeight;
|
|
447
|
-
if (delta > 0) {
|
|
448
|
-
baselineOffset += delta / 2;
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
if (paragraph != nil) {
|
|
452
|
-
attributes[NSParagraphStyleAttributeName] = paragraph;
|
|
453
|
-
}
|
|
454
|
-
if (baselineOffset != 0) {
|
|
455
|
-
attributes[NSBaselineOffsetAttributeName] = @(baselineOffset);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
if (link.length > 0) {
|
|
459
|
-
attributes[NSForegroundColorAttributeName] = _linkColor;
|
|
460
|
-
attributes[NSUnderlineStyleAttributeName] = @(NSUnderlineStyleSingle);
|
|
461
|
-
attributes[JMDEditorLinkAttribute] = link;
|
|
462
|
-
if (atomic) {
|
|
463
|
-
attributes[JMDEditorAtomicAttribute] = @YES;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
if (flags != 0) {
|
|
467
|
-
attributes[JMDEditorMarksAttribute] = @(flags);
|
|
468
|
-
}
|
|
469
|
-
if (block != 0) {
|
|
470
|
-
attributes[JMDEditorBlockAttribute] = @(block);
|
|
471
|
-
}
|
|
472
|
-
return attributes;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
- (NSMutableParagraphStyle *)paragraphStyleForBlock:(uint32_t)block {
|
|
476
|
-
const auto blockType = JMDBlockType(block);
|
|
477
|
-
if (blockType != jetmarkdown::EditorBlockType::Quote && !JMDBlockIsList(block)) {
|
|
478
|
-
return nil;
|
|
479
|
-
}
|
|
480
|
-
NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle alloc] init];
|
|
481
|
-
const CGFloat indent =
|
|
482
|
-
blockType == jetmarkdown::EditorBlockType::Quote ? 16 : 28;
|
|
483
|
-
paragraph.firstLineHeadIndent = indent;
|
|
484
|
-
paragraph.headIndent = indent;
|
|
485
|
-
return paragraph;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// Full attributes for a run described by an existing attribute dictionary.
|
|
489
|
-
- (NSDictionary<NSAttributedStringKey, id> *)attributesFromExisting:
|
|
490
|
-
(NSDictionary *)attrs
|
|
491
|
-
withFlags:(uint32_t)flags
|
|
492
|
-
block:(uint32_t)block {
|
|
493
|
-
return [self attributesForFlags:flags
|
|
494
|
-
block:block
|
|
495
|
-
link:attrs[JMDEditorLinkAttribute]
|
|
496
|
-
atomic:[attrs[JMDEditorAtomicAttribute] boolValue]];
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// Rebuilds display attributes from the data attributes (marks + block).
|
|
500
|
-
- (void)refreshDisplayAttributesInRange:(NSRange)range {
|
|
501
|
-
if (range.length == 0) {
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
|
-
NSTextStorage *storage = _textView.textStorage;
|
|
505
|
-
[storage beginEditing];
|
|
506
|
-
[storage enumerateAttributesInRange:range
|
|
507
|
-
options:0
|
|
508
|
-
usingBlock:^(NSDictionary *attrs, NSRange runRange, BOOL *stop) {
|
|
509
|
-
const uint32_t flags =
|
|
510
|
-
JMDFlagsFromValue(attrs[JMDEditorMarksAttribute]);
|
|
511
|
-
const uint32_t block =
|
|
512
|
-
JMDFlagsFromValue(attrs[JMDEditorBlockAttribute]);
|
|
513
|
-
[storage setAttributes:[self attributesFromExisting:attrs
|
|
514
|
-
withFlags:flags
|
|
515
|
-
block:block]
|
|
516
|
-
range:runRange];
|
|
517
|
-
}];
|
|
518
|
-
[storage endEditing];
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
- (void)applyTypingAttributes {
|
|
522
|
-
_textView.typingAttributes = [self attributesForFlags:_typingFlags
|
|
523
|
-
block:_typingBlock];
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// Autocorrect/autocapitalize/spellcheck follow the caret: suppressed in
|
|
527
|
-
// code contexts (`let` must not become `Let`), restored from props outside.
|
|
528
|
-
- (void)updateInputTraits {
|
|
529
|
-
const BOOL inCode =
|
|
530
|
-
JMDBlockType(_typingBlock) == jetmarkdown::EditorBlockType::Code ||
|
|
531
|
-
(_typingFlags & jetmarkdown::MarkInlineCode) != 0;
|
|
532
|
-
const UITextAutocorrectionType correction = (!inCode && _propAutoCorrect)
|
|
533
|
-
? UITextAutocorrectionTypeDefault
|
|
534
|
-
: UITextAutocorrectionTypeNo;
|
|
535
|
-
const UITextAutocapitalizationType capitalization =
|
|
536
|
-
inCode ? UITextAutocapitalizationTypeNone : _propAutoCapitalize;
|
|
537
|
-
const UITextSpellCheckingType spelling =
|
|
538
|
-
inCode ? UITextSpellCheckingTypeNo : UITextSpellCheckingTypeDefault;
|
|
539
|
-
if (_textView.autocorrectionType == correction &&
|
|
540
|
-
_textView.autocapitalizationType == capitalization &&
|
|
541
|
-
_textView.spellCheckingType == spelling) {
|
|
542
|
-
return;
|
|
543
|
-
}
|
|
544
|
-
_textView.autocorrectionType = correction;
|
|
545
|
-
_textView.autocapitalizationType = capitalization;
|
|
546
|
-
_textView.spellCheckingType = spelling;
|
|
547
|
-
if (_textView.isFirstResponder && self.window != nil) {
|
|
548
|
-
// reloadInputViews alone leaves an already-latched shift key engaged
|
|
549
|
-
// (the first code character would still capitalize); cycling the
|
|
550
|
-
// responder resets the keyboard's state. Focus/blur events are
|
|
551
|
-
// suppressed — JS must not see a blip.
|
|
552
|
-
_suppressFocusEvents = YES;
|
|
553
|
-
__block BOOL refocused = NO;
|
|
554
|
-
[UIView performWithoutAnimation:^{
|
|
555
|
-
[self->_textView resignFirstResponder];
|
|
556
|
-
refocused = [self->_textView becomeFirstResponder];
|
|
557
|
-
}];
|
|
558
|
-
_suppressFocusEvents = NO;
|
|
559
|
-
if (!refocused) {
|
|
560
|
-
// The keyboard is gone for real; JS must not believe the editor is
|
|
561
|
-
// still focused.
|
|
562
|
-
if (const auto *emitter = [self editorEventEmitter]) {
|
|
563
|
-
emitter->onEditorBlur({});
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
#pragma mark - Lines
|
|
570
|
-
|
|
571
|
-
- (NSRange)contentRangeOfLineAt:(NSUInteger)location {
|
|
572
|
-
NSString *text = _textView.text;
|
|
573
|
-
NSUInteger start = 0;
|
|
574
|
-
NSUInteger contentsEnd = 0;
|
|
575
|
-
const NSRange probe = NSMakeRange(MIN(location, text.length), 0);
|
|
576
|
-
[text getLineStart:&start end:nil contentsEnd:&contentsEnd forRange:probe];
|
|
577
|
-
return NSMakeRange(start, contentsEnd - start);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// Line-iteration primitive safe for every terminator (\n, \r, \r\n):
|
|
581
|
-
// reports the line STARTING at `location` and where the next line begins.
|
|
582
|
-
// Returns NO when `location` is not a line start (iteration is done). The
|
|
583
|
-
// old "NSMaxRange(content) + 1" advance assumed 1-char terminators and
|
|
584
|
-
// looped forever on \r\n.
|
|
585
|
-
- (BOOL)lineStartingAt:(NSUInteger)location
|
|
586
|
-
content:(NSRange *)outContent
|
|
587
|
-
nextLine:(NSUInteger *)outNext {
|
|
588
|
-
NSString *text = _textView.text;
|
|
589
|
-
if (location > text.length) {
|
|
590
|
-
return NO;
|
|
591
|
-
}
|
|
592
|
-
NSUInteger start = 0;
|
|
593
|
-
NSUInteger end = 0;
|
|
594
|
-
NSUInteger contentsEnd = 0;
|
|
595
|
-
[text getLineStart:&start
|
|
596
|
-
end:&end
|
|
597
|
-
contentsEnd:&contentsEnd
|
|
598
|
-
forRange:NSMakeRange(location, 0)];
|
|
599
|
-
if (start != location) {
|
|
600
|
-
return NO;
|
|
601
|
-
}
|
|
602
|
-
*outContent = NSMakeRange(start, contentsEnd - start);
|
|
603
|
-
*outNext = end;
|
|
604
|
-
return YES;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
- (uint32_t)blockOfLineAt:(NSUInteger)location {
|
|
608
|
-
const NSRange content = [self contentRangeOfLineAt:location];
|
|
609
|
-
if (content.length == 0) {
|
|
610
|
-
return 0;
|
|
611
|
-
}
|
|
612
|
-
return JMDFlagsFromValue([_textView.textStorage attribute:JMDEditorBlockAttribute
|
|
613
|
-
atIndex:content.location
|
|
614
|
-
effectiveRange:nil]);
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// Sets the block on every line the range touches, preserving per-character
|
|
618
|
-
// marks.
|
|
619
|
-
- (void)applyBlock:(uint32_t)block toLinesInRange:(NSRange)range {
|
|
620
|
-
NSString *text = _textView.text;
|
|
621
|
-
const NSRange lines = [text lineRangeForRange:range];
|
|
622
|
-
if (lines.length == 0) {
|
|
623
|
-
return;
|
|
624
|
-
}
|
|
625
|
-
NSTextStorage *storage = _textView.textStorage;
|
|
626
|
-
// A code fence carries raw text only: marks and links on lines converted
|
|
627
|
-
// to a code block would be dropped by the serializer, so shed them now.
|
|
628
|
-
const BOOL toCode = JMDBlockType(block) == jetmarkdown::EditorBlockType::Code;
|
|
629
|
-
[storage beginEditing];
|
|
630
|
-
[storage enumerateAttributesInRange:lines
|
|
631
|
-
options:0
|
|
632
|
-
usingBlock:^(NSDictionary *attrs, NSRange runRange, BOOL *stop) {
|
|
633
|
-
const uint32_t flags =
|
|
634
|
-
JMDFlagsFromValue(attrs[JMDEditorMarksAttribute]);
|
|
635
|
-
NSDictionary *next = toCode
|
|
636
|
-
? [self attributesForFlags:0 block:block]
|
|
637
|
-
: [self attributesFromExisting:attrs
|
|
638
|
-
withFlags:flags
|
|
639
|
-
block:block];
|
|
640
|
-
[storage setAttributes:next range:runRange];
|
|
641
|
-
}];
|
|
642
|
-
[storage endEditing];
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
- (void)toggleBlock:(jetmarkdown::EditorBlockType)type level:(uint8_t)level {
|
|
646
|
-
const uint32_t target = JMDPackBlock(type, level);
|
|
647
|
-
const NSRange selection = _textView.selectedRange;
|
|
648
|
-
NSString *text = _textView.text;
|
|
649
|
-
const NSRange lines =
|
|
650
|
-
text.length == 0 ? NSMakeRange(0, 0) : [text lineRangeForRange:selection];
|
|
651
|
-
|
|
652
|
-
BOOL allMatch = YES;
|
|
653
|
-
if (lines.length == 0) {
|
|
654
|
-
allMatch = _typingBlock == target;
|
|
655
|
-
} else {
|
|
656
|
-
NSUInteger cursor = lines.location;
|
|
657
|
-
NSRange content;
|
|
658
|
-
NSUInteger nextLine;
|
|
659
|
-
while (cursor < NSMaxRange(lines) &&
|
|
660
|
-
[self lineStartingAt:cursor content:&content nextLine:&nextLine]) {
|
|
661
|
-
if (content.length > 0 && [self blockOfLineAt:cursor] != target) {
|
|
662
|
-
allMatch = NO;
|
|
663
|
-
break;
|
|
664
|
-
}
|
|
665
|
-
if (content.length == 0 && _typingBlock != target) {
|
|
666
|
-
allMatch = NO;
|
|
667
|
-
break;
|
|
668
|
-
}
|
|
669
|
-
if (nextLine == cursor) {
|
|
670
|
-
break;
|
|
671
|
-
}
|
|
672
|
-
cursor = nextLine;
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
const uint32_t next = allMatch ? 0 : target;
|
|
677
|
-
if (lines.length > 0) {
|
|
678
|
-
[self applyBlock:next toLinesInRange:lines];
|
|
679
|
-
_textView.selectedRange = selection;
|
|
680
|
-
}
|
|
681
|
-
_typingBlock = next;
|
|
682
|
-
if (JMDBlockType(next) == jetmarkdown::EditorBlockType::Code) {
|
|
683
|
-
// Armed marks cannot survive inside a code fence.
|
|
684
|
-
_typingFlags = 0;
|
|
685
|
-
}
|
|
686
|
-
[self applyTypingAttributes];
|
|
687
|
-
[self updateInputTraits];
|
|
688
|
-
[self invalidateDecorations];
|
|
689
|
-
[self textContentChanged];
|
|
690
|
-
[self emitState];
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
#pragma mark - Markers
|
|
694
|
-
|
|
695
|
-
- (void)invalidateDecorations {
|
|
696
|
-
[_markerView setNeedsDisplay];
|
|
697
|
-
[_codeBackgroundView setNeedsDisplay];
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
// The block shown for a line: stored attribute for content lines; for the
|
|
701
|
-
// EMPTY caret line, the armed typing block (immediate feedback on toggle).
|
|
702
|
-
- (uint32_t)displayBlockForLineContent:(NSRange)content {
|
|
703
|
-
if (content.length > 0) {
|
|
704
|
-
return [self blockOfLineAt:content.location];
|
|
705
|
-
}
|
|
706
|
-
const NSRange selection = _textView.selectedRange;
|
|
707
|
-
if (selection.length == 0 && selection.location == content.location) {
|
|
708
|
-
return _typingBlock;
|
|
709
|
-
}
|
|
710
|
-
return 0;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
- (CGRect)rectForLineContent:(NSRange)content {
|
|
714
|
-
if (content.length > 0) {
|
|
715
|
-
NSLayoutManager *layoutManager = _textView.layoutManager;
|
|
716
|
-
const NSRange glyphs =
|
|
717
|
-
[layoutManager glyphRangeForCharacterRange:content actualCharacterRange:nil];
|
|
718
|
-
CGRect rect = [layoutManager boundingRectForGlyphRange:glyphs
|
|
719
|
-
inTextContainer:_textView.textContainer];
|
|
720
|
-
rect.origin.x += _textView.textContainerInset.left;
|
|
721
|
-
rect.origin.y += _textView.textContainerInset.top;
|
|
722
|
-
return rect;
|
|
723
|
-
}
|
|
724
|
-
UITextPosition *position =
|
|
725
|
-
[_textView positionFromPosition:_textView.beginningOfDocument
|
|
726
|
-
offset:(NSInteger)content.location];
|
|
727
|
-
if (position == nil) {
|
|
728
|
-
return CGRectZero;
|
|
729
|
-
}
|
|
730
|
-
return [_textView caretRectForPosition:position];
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
- (void)drawMarkersInContext:(CGContextRef)context view:(JMDEditorMarkerView *)view {
|
|
734
|
-
if (context == nil) {
|
|
735
|
-
return;
|
|
736
|
-
}
|
|
737
|
-
NSString *text = _textView.text;
|
|
738
|
-
const UIEdgeInsets inset = _textView.textContainerInset;
|
|
739
|
-
UIColor *markerColor = [_baseColor colorWithAlphaComponent:0.6];
|
|
740
|
-
|
|
741
|
-
NSDictionary *markerAttributes = @{
|
|
742
|
-
NSFontAttributeName : _baseFont,
|
|
743
|
-
NSForegroundColorAttributeName : markerColor,
|
|
744
|
-
};
|
|
745
|
-
NSUInteger location = 0;
|
|
746
|
-
NSInteger orderedNumber = 0;
|
|
747
|
-
NSRange content;
|
|
748
|
-
NSUInteger nextLine;
|
|
749
|
-
while ([self lineStartingAt:location content:&content nextLine:&nextLine]) {
|
|
750
|
-
const uint32_t block = [self displayBlockForLineContent:content];
|
|
751
|
-
const auto type = JMDBlockType(block);
|
|
752
|
-
|
|
753
|
-
if (type == jetmarkdown::EditorBlockType::Ordered) {
|
|
754
|
-
orderedNumber += 1;
|
|
755
|
-
} else {
|
|
756
|
-
orderedNumber = 0;
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
if (block != 0) {
|
|
760
|
-
const CGRect lineRect = [self rectForLineContent:content];
|
|
761
|
-
if (!CGRectIsEmpty(lineRect)) {
|
|
762
|
-
const CGFloat top = lineRect.origin.y;
|
|
763
|
-
|
|
764
|
-
if (type == jetmarkdown::EditorBlockType::Quote) {
|
|
765
|
-
[markerColor setFill];
|
|
766
|
-
UIRectFill(CGRectMake(inset.left + 4, top, 3, lineRect.size.height));
|
|
767
|
-
} else if (type == jetmarkdown::EditorBlockType::Bullet ||
|
|
768
|
-
type == jetmarkdown::EditorBlockType::Ordered) {
|
|
769
|
-
NSString *marker = type == jetmarkdown::EditorBlockType::Bullet
|
|
770
|
-
? @"•"
|
|
771
|
-
: [NSString stringWithFormat:@"%ld.", (long)orderedNumber];
|
|
772
|
-
const CGSize size = [marker sizeWithAttributes:markerAttributes];
|
|
773
|
-
[marker drawAtPoint:CGPointMake(inset.left + 24 - size.width - 6, top)
|
|
774
|
-
withAttributes:markerAttributes];
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
if (nextLine == location) {
|
|
780
|
-
break;
|
|
781
|
-
}
|
|
782
|
-
location = nextLine;
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
- (void)drawCodeBackgroundsInView:(JMDEditorCodeBackgroundView *)view {
|
|
787
|
-
NSString *text = _textView.text;
|
|
788
|
-
const UIEdgeInsets inset = _textView.textContainerInset;
|
|
789
|
-
const CGFloat left = MAX(inset.left - 6, 0);
|
|
790
|
-
const CGFloat width = view.bounds.size.width - left - MAX(inset.right - 6, 0);
|
|
791
|
-
UIColor *fill = [UIColor colorWithWhite:0.5 alpha:0.1];
|
|
792
|
-
|
|
793
|
-
// Contiguous code lines merge into one rounded stripe.
|
|
794
|
-
CGFloat groupTop = 0;
|
|
795
|
-
CGFloat groupBottom = 0;
|
|
796
|
-
BOOL inGroup = NO;
|
|
797
|
-
const auto flush = [&]() {
|
|
798
|
-
if (inGroup) {
|
|
799
|
-
UIBezierPath *path = [UIBezierPath
|
|
800
|
-
bezierPathWithRoundedRect:CGRectMake(left, groupTop - 2, width,
|
|
801
|
-
groupBottom - groupTop + 4)
|
|
802
|
-
cornerRadius:6];
|
|
803
|
-
[fill setFill];
|
|
804
|
-
[path fill];
|
|
805
|
-
inGroup = NO;
|
|
806
|
-
}
|
|
807
|
-
};
|
|
808
|
-
|
|
809
|
-
NSUInteger location = 0;
|
|
810
|
-
NSRange content;
|
|
811
|
-
NSUInteger nextLine;
|
|
812
|
-
while ([self lineStartingAt:location content:&content nextLine:&nextLine]) {
|
|
813
|
-
const uint32_t block = [self displayBlockForLineContent:content];
|
|
814
|
-
const CGRect lineRect = JMDBlockType(block) == jetmarkdown::EditorBlockType::Code
|
|
815
|
-
? [self rectForLineContent:content]
|
|
816
|
-
: CGRectZero;
|
|
817
|
-
|
|
818
|
-
if (!CGRectIsEmpty(lineRect)) {
|
|
819
|
-
if (!inGroup) {
|
|
820
|
-
inGroup = YES;
|
|
821
|
-
groupTop = lineRect.origin.y;
|
|
822
|
-
}
|
|
823
|
-
groupBottom = CGRectGetMaxY(lineRect);
|
|
824
|
-
} else {
|
|
825
|
-
flush();
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
if (nextLine == location) {
|
|
829
|
-
break;
|
|
830
|
-
}
|
|
831
|
-
location = nextLine;
|
|
832
|
-
}
|
|
833
|
-
flush();
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
#pragma mark - Marks
|
|
837
|
-
|
|
838
|
-
// Marks present across the ENTIRE range (the AND), which drives both toggle
|
|
839
|
-
// direction and the reported selection state.
|
|
840
|
-
- (uint32_t)commonFlagsInRange:(NSRange)range {
|
|
841
|
-
__block uint32_t common = ~0u;
|
|
842
|
-
[_textView.textStorage enumerateAttribute:JMDEditorMarksAttribute
|
|
843
|
-
inRange:range
|
|
844
|
-
options:0
|
|
845
|
-
usingBlock:^(id value, NSRange runRange, BOOL *stop) {
|
|
846
|
-
common &= JMDFlagsFromValue(value);
|
|
847
|
-
}];
|
|
848
|
-
return common == ~0u ? 0 : common;
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
- (uint32_t)flagsBeforeCaret:(NSUInteger)location {
|
|
852
|
-
NSTextStorage *storage = _textView.textStorage;
|
|
853
|
-
if (storage.length == 0) {
|
|
854
|
-
return 0;
|
|
855
|
-
}
|
|
856
|
-
const NSUInteger probe = location > 0 ? location - 1 : 0;
|
|
857
|
-
if (probe >= storage.length) {
|
|
858
|
-
return 0;
|
|
859
|
-
}
|
|
860
|
-
// Marks end at the paragraph break: a newline never carries them forward
|
|
861
|
-
// (otherwise a mark armed once would leak into every following line).
|
|
862
|
-
if ([_textView.text characterAtIndex:probe] == '\n') {
|
|
863
|
-
return 0;
|
|
864
|
-
}
|
|
865
|
-
return JMDFlagsFromValue([storage attribute:JMDEditorMarksAttribute
|
|
866
|
-
atIndex:probe
|
|
867
|
-
effectiveRange:nil]);
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
// YES when the caret's armed block or any line the range touches is a code
|
|
871
|
-
// block.
|
|
872
|
-
- (BOOL)selectionTouchesCodeBlock:(NSRange)range {
|
|
873
|
-
if (JMDBlockType(_typingBlock) == jetmarkdown::EditorBlockType::Code) {
|
|
874
|
-
return YES;
|
|
875
|
-
}
|
|
876
|
-
NSString *text = _textView.text;
|
|
877
|
-
NSUInteger lineStart = 0;
|
|
878
|
-
[text getLineStart:&lineStart
|
|
879
|
-
end:nil
|
|
880
|
-
contentsEnd:nil
|
|
881
|
-
forRange:NSMakeRange(MIN(range.location, text.length), 0)];
|
|
882
|
-
NSUInteger location = lineStart;
|
|
883
|
-
const NSUInteger max = NSMaxRange(range);
|
|
884
|
-
NSRange content;
|
|
885
|
-
NSUInteger nextLine;
|
|
886
|
-
while ([self lineStartingAt:location content:&content nextLine:&nextLine]) {
|
|
887
|
-
if (JMDBlockType([self blockOfLineAt:location]) ==
|
|
888
|
-
jetmarkdown::EditorBlockType::Code) {
|
|
889
|
-
return YES;
|
|
890
|
-
}
|
|
891
|
-
if (nextLine == location || nextLine > max) {
|
|
892
|
-
break;
|
|
893
|
-
}
|
|
894
|
-
location = nextLine;
|
|
895
|
-
}
|
|
896
|
-
return NO;
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
- (void)toggleMark:(uint32_t)mark {
|
|
900
|
-
const NSRange selection = _textView.selectedRange;
|
|
901
|
-
// A code fence carries raw text only — marks applied there would render
|
|
902
|
-
// in the editor but silently vanish from the markdown, so refuse them.
|
|
903
|
-
if ([self selectionTouchesCodeBlock:selection]) {
|
|
904
|
-
return;
|
|
905
|
-
}
|
|
906
|
-
// Superscript and subscript are mutually exclusive: a glyph cannot sit
|
|
907
|
-
// above and below the baseline, and combined they serialize to nested
|
|
908
|
-
// ^~…~^ that does not round-trip.
|
|
909
|
-
uint32_t exclusive = 0;
|
|
910
|
-
if (mark == jetmarkdown::MarkSuperscript) {
|
|
911
|
-
exclusive = jetmarkdown::MarkSubscript;
|
|
912
|
-
} else if (mark == jetmarkdown::MarkSubscript) {
|
|
913
|
-
exclusive = jetmarkdown::MarkSuperscript;
|
|
914
|
-
}
|
|
915
|
-
if (selection.length == 0) {
|
|
916
|
-
_typingFlags ^= mark;
|
|
917
|
-
if ((_typingFlags & mark) != 0) {
|
|
918
|
-
_typingFlags &= ~exclusive;
|
|
919
|
-
}
|
|
920
|
-
[self applyTypingAttributes];
|
|
921
|
-
[self updateInputTraits];
|
|
922
|
-
[self emitState];
|
|
923
|
-
return;
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
const BOOL allHave = ([self commonFlagsInRange:selection] & mark) != 0;
|
|
927
|
-
NSTextStorage *storage = _textView.textStorage;
|
|
928
|
-
[storage beginEditing];
|
|
929
|
-
[storage enumerateAttributesInRange:selection
|
|
930
|
-
options:0
|
|
931
|
-
usingBlock:^(NSDictionary *attrs, NSRange runRange, BOOL *stop) {
|
|
932
|
-
const uint32_t flags =
|
|
933
|
-
JMDFlagsFromValue(attrs[JMDEditorMarksAttribute]);
|
|
934
|
-
const uint32_t block =
|
|
935
|
-
JMDFlagsFromValue(attrs[JMDEditorBlockAttribute]);
|
|
936
|
-
const uint32_t next = allHave
|
|
937
|
-
? (flags & ~mark)
|
|
938
|
-
: ((flags | mark) & ~exclusive);
|
|
939
|
-
[storage setAttributes:[self attributesFromExisting:attrs
|
|
940
|
-
withFlags:next
|
|
941
|
-
block:block]
|
|
942
|
-
range:runRange];
|
|
943
|
-
}];
|
|
944
|
-
[storage endEditing];
|
|
945
|
-
_textView.selectedRange = selection;
|
|
946
|
-
[self textContentChanged];
|
|
947
|
-
[self emitState];
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
#pragma mark - Fabric plumbing
|
|
951
|
-
|
|
952
|
-
- (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState {
|
|
953
|
-
_state = std::static_pointer_cast<const JetMarkdownEditorShadowNode::ConcreteState>(state);
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps {
|
|
957
|
-
const auto &newProps = *std::static_pointer_cast<JetMarkdownEditorProps const>(props);
|
|
958
|
-
const auto &prevProps = *std::static_pointer_cast<JetMarkdownEditorProps const>(_props);
|
|
959
|
-
|
|
960
|
-
NSString *stylesJson = JMDStringFromCpp(newProps.stylesJson);
|
|
961
|
-
if (![stylesJson isEqualToString:_stylesJson]) {
|
|
962
|
-
_stylesJson = stylesJson;
|
|
963
|
-
[self applyTextStyles];
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
if (!_defaultValueApplied) {
|
|
967
|
-
_defaultValueApplied = YES;
|
|
968
|
-
if (!newProps.defaultValue.empty()) {
|
|
969
|
-
[self applyMarkdownValue:newProps.defaultValue];
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
_textView.editable = newProps.editable;
|
|
974
|
-
_propScrollEnabled = newProps.scrollEnabled;
|
|
975
|
-
_maxHeight = newProps.maxHeight;
|
|
976
|
-
_multiline = newProps.multiline;
|
|
977
|
-
[self publishHeight];
|
|
978
|
-
|
|
979
|
-
if (newProps.allowFontScaling != _allowFontScaling) {
|
|
980
|
-
_allowFontScaling = newProps.allowFontScaling;
|
|
981
|
-
[self applyTextStyles];
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
_propAutoCorrect = newProps.autoCorrect;
|
|
985
|
-
switch (newProps.autoCapitalize) {
|
|
986
|
-
case JetMarkdownEditorAutoCapitalize::None:
|
|
987
|
-
_propAutoCapitalize = UITextAutocapitalizationTypeNone;
|
|
988
|
-
break;
|
|
989
|
-
case JetMarkdownEditorAutoCapitalize::Words:
|
|
990
|
-
_propAutoCapitalize = UITextAutocapitalizationTypeWords;
|
|
991
|
-
break;
|
|
992
|
-
case JetMarkdownEditorAutoCapitalize::Characters:
|
|
993
|
-
_propAutoCapitalize = UITextAutocapitalizationTypeAllCharacters;
|
|
994
|
-
break;
|
|
995
|
-
case JetMarkdownEditorAutoCapitalize::Sentences:
|
|
996
|
-
_propAutoCapitalize = UITextAutocapitalizationTypeSentences;
|
|
997
|
-
break;
|
|
998
|
-
}
|
|
999
|
-
[self updateInputTraits];
|
|
1000
|
-
|
|
1001
|
-
// UIKit shares one tint for the caret and the selection highlight;
|
|
1002
|
-
// selectionColor wins when both are set, nil restores the system tint.
|
|
1003
|
-
// SharedColor carries platform colors (PlatformColor/DynamicColorIOS)
|
|
1004
|
-
// through as dynamic-provider UIColors.
|
|
1005
|
-
UIColor *selectionColor = RCTUIColorFromSharedColor(newProps.selectionColor);
|
|
1006
|
-
UIColor *cursorColor = RCTUIColorFromSharedColor(newProps.cursorColor);
|
|
1007
|
-
_textView.tintColor = selectionColor ?: cursorColor;
|
|
1008
|
-
|
|
1009
|
-
NSMutableArray<NSString *> *triggers = [NSMutableArray array];
|
|
1010
|
-
for (const auto &trigger : newProps.mentionTriggers) {
|
|
1011
|
-
NSString *value = JMDStringFromCpp(trigger);
|
|
1012
|
-
if (value.length > 0) {
|
|
1013
|
-
[triggers addObject:[value substringWithRange:
|
|
1014
|
-
[value rangeOfComposedCharacterSequenceAtIndex:0]]];
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
_mentionTriggers = triggers;
|
|
1018
|
-
|
|
1019
|
-
NSString *placeholder = JMDStringFromCpp(newProps.placeholder);
|
|
1020
|
-
if (![placeholder isEqualToString:_placeholderLabel.text]) {
|
|
1021
|
-
_placeholderLabel.text = placeholder;
|
|
1022
|
-
_textView.accessibilityLabel = placeholder;
|
|
1023
|
-
[self setNeedsLayout];
|
|
1024
|
-
}
|
|
1025
|
-
UIColor *placeholderColor =
|
|
1026
|
-
RCTUIColorFromSharedColor(newProps.placeholderTextColor);
|
|
1027
|
-
_placeholderLabel.textColor =
|
|
1028
|
-
placeholderColor ?: [UIColor colorWithWhite:0 alpha:0.3];
|
|
1029
|
-
|
|
1030
|
-
if (newProps.autoFocus && !prevProps.autoFocus) {
|
|
1031
|
-
_autoFocusHandled = NO;
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
[super updateProps:props oldProps:oldProps];
|
|
1035
|
-
[self refreshPlaceholderVisibility];
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
|
|
1039
|
-
[super traitCollectionDidChange:previousTraitCollection];
|
|
1040
|
-
if ([self.traitCollection
|
|
1041
|
-
hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
|
|
1042
|
-
// Marker/stripe decorations draw with (possibly dynamic) UIColors;
|
|
1043
|
-
// re-resolve them under the new appearance.
|
|
1044
|
-
[self invalidateDecorations];
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
- (void)didMoveToWindow {
|
|
1049
|
-
[super didMoveToWindow];
|
|
1050
|
-
const auto &props = *std::static_pointer_cast<JetMarkdownEditorProps const>(_props);
|
|
1051
|
-
if (self.window != nil && props.autoFocus && !_autoFocusHandled) {
|
|
1052
|
-
_autoFocusHandled = YES;
|
|
1053
|
-
[_textView becomeFirstResponder];
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
- (void)dealloc {
|
|
1058
|
-
[NSNotificationCenter.defaultCenter removeObserver:self];
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
- (void)jmdContentSizeCategoryDidChange {
|
|
1062
|
-
if (_allowFontScaling) {
|
|
1063
|
-
[self applyTextStyles];
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
- (void)prepareForRecycle {
|
|
1068
|
-
[super prepareForRecycle];
|
|
1069
|
-
_textView.text = @"";
|
|
1070
|
-
_stylesJson = @"";
|
|
1071
|
-
_defaultValueApplied = NO;
|
|
1072
|
-
_autoFocusHandled = NO;
|
|
1073
|
-
_lastPublishedHeight = 0;
|
|
1074
|
-
_typingFlags = 0;
|
|
1075
|
-
_typingBlock = 0;
|
|
1076
|
-
_paragraphAfterNewline = NO;
|
|
1077
|
-
_mentionActive = NO;
|
|
1078
|
-
_lastMentionQuery = nil;
|
|
1079
|
-
_lastSelection = NSMakeRange(0, 0);
|
|
1080
|
-
_stateEmitted = NO;
|
|
1081
|
-
_state = nullptr;
|
|
1082
|
-
[self applyTextStyles];
|
|
1083
|
-
[self refreshPlaceholderVisibility];
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
- (void)layoutSubviews {
|
|
1087
|
-
[super layoutSubviews];
|
|
1088
|
-
_textView.frame = self.bounds;
|
|
1089
|
-
_markerView.frame = self.bounds;
|
|
1090
|
-
_codeBackgroundView.frame = self.bounds;
|
|
1091
|
-
|
|
1092
|
-
const UIEdgeInsets inset = _textView.textContainerInset;
|
|
1093
|
-
const CGSize placeholderSize = [_placeholderLabel sizeThatFits:CGSizeMake(
|
|
1094
|
-
self.bounds.size.width - inset.left - inset.right, CGFLOAT_MAX)];
|
|
1095
|
-
_placeholderLabel.frame = CGRectMake(
|
|
1096
|
-
inset.left, inset.top, placeholderSize.width, placeholderSize.height);
|
|
1097
|
-
|
|
1098
|
-
[self invalidateDecorations];
|
|
1099
|
-
[self publishHeight];
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
#pragma mark - Autogrow
|
|
1103
|
-
|
|
1104
|
-
- (void)publishHeight {
|
|
1105
|
-
const CGFloat width = self.bounds.size.width;
|
|
1106
|
-
if (width <= 0 || _state == nullptr) {
|
|
1107
|
-
return;
|
|
1108
|
-
}
|
|
1109
|
-
const CGSize size = [_textView sizeThatFits:CGSizeMake(width, CGFLOAT_MAX)];
|
|
1110
|
-
CGFloat height = size.height;
|
|
1111
|
-
BOOL exceedsMax = NO;
|
|
1112
|
-
if (_maxHeight > 0 && height > _maxHeight) {
|
|
1113
|
-
height = _maxHeight;
|
|
1114
|
-
exceedsMax = YES;
|
|
1115
|
-
}
|
|
1116
|
-
// Grow-then-scroll: once content passes maxHeight the text view scrolls
|
|
1117
|
-
// internally like a textarea.
|
|
1118
|
-
const BOOL wantScroll = exceedsMax || _propScrollEnabled;
|
|
1119
|
-
if (_textView.scrollEnabled != wantScroll) {
|
|
1120
|
-
_textView.scrollEnabled = wantScroll;
|
|
1121
|
-
}
|
|
1122
|
-
if (fabs(height - _lastPublishedHeight) < 0.5) {
|
|
1123
|
-
return;
|
|
1124
|
-
}
|
|
1125
|
-
_lastPublishedHeight = height;
|
|
1126
|
-
_state->updateState(JetMarkdownEditorState(height));
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
#pragma mark - Events
|
|
1130
|
-
|
|
1131
|
-
- (const JetMarkdownEditorEventEmitter *)editorEventEmitter {
|
|
1132
|
-
if (!_eventEmitter) {
|
|
1133
|
-
return nullptr;
|
|
1134
|
-
}
|
|
1135
|
-
return static_cast<const JetMarkdownEditorEventEmitter *>(_eventEmitter.get());
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
- (std::string)serializedMarkdown {
|
|
1139
|
-
NSTextStorage *storage = _textView.textStorage;
|
|
1140
|
-
NSString *text = _textView.text;
|
|
1141
|
-
const std::string utf8(text.UTF8String ?: "");
|
|
1142
|
-
|
|
1143
|
-
__block std::vector<jetmarkdown::StyledRun> runs;
|
|
1144
|
-
[storage enumerateAttribute:JMDEditorMarksAttribute
|
|
1145
|
-
inRange:NSMakeRange(0, storage.length)
|
|
1146
|
-
options:0
|
|
1147
|
-
usingBlock:^(id value, NSRange runRange, BOOL *stop) {
|
|
1148
|
-
const uint32_t flags = JMDFlagsFromValue(value);
|
|
1149
|
-
if (flags != 0) {
|
|
1150
|
-
runs.push_back(
|
|
1151
|
-
{static_cast<uint32_t>(runRange.location),
|
|
1152
|
-
static_cast<uint32_t>(NSMaxRange(runRange)),
|
|
1153
|
-
flags});
|
|
1154
|
-
}
|
|
1155
|
-
}];
|
|
1156
|
-
|
|
1157
|
-
std::vector<jetmarkdown::EditorLine> lines;
|
|
1158
|
-
NSUInteger location = 0;
|
|
1159
|
-
NSRange content;
|
|
1160
|
-
NSUInteger nextLine;
|
|
1161
|
-
while ([self lineStartingAt:location content:&content nextLine:&nextLine]) {
|
|
1162
|
-
const uint32_t block =
|
|
1163
|
-
content.length > 0 ? [self blockOfLineAt:content.location] : 0;
|
|
1164
|
-
lines.push_back(
|
|
1165
|
-
{JMDBlockType(block), static_cast<uint8_t>(block & 0xFF)});
|
|
1166
|
-
if (nextLine == location) {
|
|
1167
|
-
break;
|
|
1168
|
-
}
|
|
1169
|
-
location = nextLine;
|
|
1170
|
-
}
|
|
1171
|
-
if (lines.empty()) {
|
|
1172
|
-
lines.push_back({});
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
__block std::vector<jetmarkdown::LinkRun> links;
|
|
1176
|
-
[storage enumerateAttribute:JMDEditorLinkAttribute
|
|
1177
|
-
inRange:NSMakeRange(0, storage.length)
|
|
1178
|
-
options:0
|
|
1179
|
-
usingBlock:^(id value, NSRange runRange, BOOL *stop) {
|
|
1180
|
-
NSString *url = (NSString *)value;
|
|
1181
|
-
if (url.length > 0) {
|
|
1182
|
-
links.push_back(
|
|
1183
|
-
{static_cast<uint32_t>(runRange.location),
|
|
1184
|
-
static_cast<uint32_t>(NSMaxRange(runRange)),
|
|
1185
|
-
std::string(url.UTF8String ?: "")});
|
|
1186
|
-
}
|
|
1187
|
-
}];
|
|
1188
|
-
|
|
1189
|
-
return jetmarkdown::markdownFromEditor(utf8, runs, lines, links);
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
- (void)textContentChanged {
|
|
1193
|
-
[self refreshPlaceholderVisibility];
|
|
1194
|
-
[self publishHeight];
|
|
1195
|
-
[self invalidateDecorations];
|
|
1196
|
-
if (const auto *emitter = [self editorEventEmitter]) {
|
|
1197
|
-
const std::string text(_textView.text.UTF8String ?: "");
|
|
1198
|
-
emitter->onEditorChangeText({.text = text});
|
|
1199
|
-
}
|
|
1200
|
-
// Serializing the whole document per keystroke is the expensive half of
|
|
1201
|
-
// this pipeline; coalesce bursts to one emission per runloop turn.
|
|
1202
|
-
if (!_markdownEmitScheduled) {
|
|
1203
|
-
_markdownEmitScheduled = YES;
|
|
1204
|
-
__weak __typeof(self) weakSelf = self;
|
|
1205
|
-
dispatch_async(dispatch_get_main_queue(), ^{
|
|
1206
|
-
__typeof(self) strongSelf = weakSelf;
|
|
1207
|
-
if (strongSelf == nil) {
|
|
1208
|
-
return;
|
|
1209
|
-
}
|
|
1210
|
-
strongSelf->_markdownEmitScheduled = NO;
|
|
1211
|
-
if (const auto *emitter = [strongSelf editorEventEmitter]) {
|
|
1212
|
-
emitter->onEditorChangeMarkdown({.markdown = [strongSelf serializedMarkdown]});
|
|
1213
|
-
}
|
|
1214
|
-
});
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
- (void)emitState {
|
|
1219
|
-
const NSRange selection = _textView.selectedRange;
|
|
1220
|
-
const uint32_t flags = selection.length == 0
|
|
1221
|
-
? _typingFlags
|
|
1222
|
-
: [self commonFlagsInRange:selection];
|
|
1223
|
-
const NSRange caretLine = [self contentRangeOfLineAt:selection.location];
|
|
1224
|
-
const uint32_t block =
|
|
1225
|
-
caretLine.length > 0 ? [self blockOfLineAt:caretLine.location] : _typingBlock;
|
|
1226
|
-
const uint64_t stateKey = (static_cast<uint64_t>(block) << 32) | flags;
|
|
1227
|
-
if (_stateEmitted && stateKey == _lastStateKey) {
|
|
1228
|
-
return;
|
|
1229
|
-
}
|
|
1230
|
-
_lastStateKey = stateKey;
|
|
1231
|
-
_stateEmitted = YES;
|
|
1232
|
-
if (const auto *emitter = [self editorEventEmitter]) {
|
|
1233
|
-
const auto type = JMDBlockType(block);
|
|
1234
|
-
emitter->onEditorChangeState({
|
|
1235
|
-
.headingLevel = type == jetmarkdown::EditorBlockType::Heading
|
|
1236
|
-
? static_cast<int>(block & 0xFF)
|
|
1237
|
-
: 0,
|
|
1238
|
-
.isBlockQuote = type == jetmarkdown::EditorBlockType::Quote,
|
|
1239
|
-
.isBold = (flags & jetmarkdown::MarkBold) != 0,
|
|
1240
|
-
.isCodeBlock = type == jetmarkdown::EditorBlockType::Code,
|
|
1241
|
-
.isInlineCode = (flags & jetmarkdown::MarkInlineCode) != 0,
|
|
1242
|
-
.isItalic = (flags & jetmarkdown::MarkItalic) != 0,
|
|
1243
|
-
.isOrderedList = type == jetmarkdown::EditorBlockType::Ordered,
|
|
1244
|
-
.isSpoiler = (flags & jetmarkdown::MarkSpoiler) != 0,
|
|
1245
|
-
.isStrikethrough = (flags & jetmarkdown::MarkStrikethrough) != 0,
|
|
1246
|
-
.isSubscript = (flags & jetmarkdown::MarkSubscript) != 0,
|
|
1247
|
-
.isSuperscript = (flags & jetmarkdown::MarkSuperscript) != 0,
|
|
1248
|
-
.isUnorderedList = type == jetmarkdown::EditorBlockType::Bullet,
|
|
1249
|
-
});
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
- (void)refreshPlaceholderVisibility {
|
|
1254
|
-
_placeholderLabel.hidden = _textView.text.length > 0;
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
#pragma mark - Links & mentions
|
|
1258
|
-
|
|
1259
|
-
static BOOL JMDIsWordBreak(unichar c) {
|
|
1260
|
-
return c == ' ' || c == '\t' || c == '\n';
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
// The atomic token range containing (or abutting) the position, if any.
|
|
1264
|
-
- (NSRange)atomicRangeAt:(NSUInteger)location {
|
|
1265
|
-
NSTextStorage *storage = _textView.textStorage;
|
|
1266
|
-
if (storage.length == 0 || location >= storage.length) {
|
|
1267
|
-
return NSMakeRange(NSNotFound, 0);
|
|
1268
|
-
}
|
|
1269
|
-
NSRange effective = NSMakeRange(NSNotFound, 0);
|
|
1270
|
-
id value = [storage attribute:JMDEditorAtomicAttribute
|
|
1271
|
-
atIndex:location
|
|
1272
|
-
longestEffectiveRange:&effective
|
|
1273
|
-
inRange:NSMakeRange(0, storage.length)];
|
|
1274
|
-
return [value boolValue] ? effective : NSMakeRange(NSNotFound, 0);
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
- (void)endMentionSession {
|
|
1278
|
-
if (!_mentionActive) {
|
|
1279
|
-
return;
|
|
1280
|
-
}
|
|
1281
|
-
_mentionActive = NO;
|
|
1282
|
-
_lastMentionQuery = nil;
|
|
1283
|
-
if (const auto *emitter = [self editorEventEmitter]) {
|
|
1284
|
-
emitter->onEditorMentionEnd(
|
|
1285
|
-
{.trigger = std::string(_mentionTrigger.UTF8String ?: "")});
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
// Runs after every content or caret change: starts, updates, or ends the
|
|
1290
|
-
// mention session based on the text between the trigger and the caret.
|
|
1291
|
-
- (void)updateMentionSession {
|
|
1292
|
-
if (_mentionTriggers.count == 0) {
|
|
1293
|
-
return;
|
|
1294
|
-
}
|
|
1295
|
-
NSString *text = _textView.text;
|
|
1296
|
-
const NSRange selection = _textView.selectedRange;
|
|
1297
|
-
if (selection.length != 0) {
|
|
1298
|
-
[self endMentionSession];
|
|
1299
|
-
return;
|
|
1300
|
-
}
|
|
1301
|
-
const NSUInteger caret = selection.location;
|
|
1302
|
-
|
|
1303
|
-
if (_mentionActive) {
|
|
1304
|
-
BOOL valid = _mentionStart < text.length && caret > _mentionStart &&
|
|
1305
|
-
caret <= text.length;
|
|
1306
|
-
if (valid) {
|
|
1307
|
-
NSString *trigger = [text substringWithRange:NSMakeRange(_mentionStart, 1)];
|
|
1308
|
-
valid = [trigger isEqualToString:_mentionTrigger];
|
|
1309
|
-
}
|
|
1310
|
-
NSString *query = @"";
|
|
1311
|
-
if (valid) {
|
|
1312
|
-
query = [text substringWithRange:NSMakeRange(
|
|
1313
|
-
_mentionStart + 1, caret - _mentionStart - 1)];
|
|
1314
|
-
for (NSUInteger i = 0; i < query.length; i++) {
|
|
1315
|
-
if (JMDIsWordBreak([query characterAtIndex:i])) {
|
|
1316
|
-
valid = NO;
|
|
1317
|
-
break;
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
if (!valid) {
|
|
1322
|
-
[self endMentionSession];
|
|
1323
|
-
return;
|
|
1324
|
-
}
|
|
1325
|
-
if ([query isEqualToString:_lastMentionQuery]) {
|
|
1326
|
-
return;
|
|
1327
|
-
}
|
|
1328
|
-
_lastMentionQuery = query;
|
|
1329
|
-
if (const auto *emitter = [self editorEventEmitter]) {
|
|
1330
|
-
emitter->onEditorMentionChange({
|
|
1331
|
-
.query = std::string(query.UTF8String ?: ""),
|
|
1332
|
-
.trigger = std::string(_mentionTrigger.UTF8String ?: ""),
|
|
1333
|
-
});
|
|
1334
|
-
}
|
|
1335
|
-
return;
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
// A trigger character at a word start (directly before the caret) opens
|
|
1339
|
-
// a session.
|
|
1340
|
-
if (caret == 0 || caret > text.length) {
|
|
1341
|
-
return;
|
|
1342
|
-
}
|
|
1343
|
-
NSString *last = [text substringWithRange:NSMakeRange(caret - 1, 1)];
|
|
1344
|
-
if (![_mentionTriggers containsObject:last]) {
|
|
1345
|
-
return;
|
|
1346
|
-
}
|
|
1347
|
-
if (caret >= 2 && !JMDIsWordBreak([text characterAtIndex:caret - 2])) {
|
|
1348
|
-
return;
|
|
1349
|
-
}
|
|
1350
|
-
_mentionActive = YES;
|
|
1351
|
-
_mentionTrigger = last;
|
|
1352
|
-
_mentionStart = caret - 1;
|
|
1353
|
-
if (const auto *emitter = [self editorEventEmitter]) {
|
|
1354
|
-
emitter->onEditorMentionStart(
|
|
1355
|
-
{.trigger = std::string(last.UTF8String ?: "")});
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
// After a word break is typed, reports a bare URL the word forms (the app
|
|
1360
|
-
// decides whether to call insertLink).
|
|
1361
|
-
- (void)detectLinkBefore:(NSUInteger)location {
|
|
1362
|
-
NSString *text = _textView.text;
|
|
1363
|
-
if (location > text.length) {
|
|
1364
|
-
return;
|
|
1365
|
-
}
|
|
1366
|
-
NSUInteger wordStart = location;
|
|
1367
|
-
while (wordStart > 0 &&
|
|
1368
|
-
!JMDIsWordBreak([text characterAtIndex:wordStart - 1])) {
|
|
1369
|
-
wordStart--;
|
|
1370
|
-
}
|
|
1371
|
-
if (wordStart >= location) {
|
|
1372
|
-
return;
|
|
1373
|
-
}
|
|
1374
|
-
NSString *word = [text substringWithRange:NSMakeRange(wordStart, location - wordStart)];
|
|
1375
|
-
if (![word hasPrefix:@"http://"] && ![word hasPrefix:@"https://"]) {
|
|
1376
|
-
return;
|
|
1377
|
-
}
|
|
1378
|
-
if ([word isEqualToString:@"http://"] || [word isEqualToString:@"https://"]) {
|
|
1379
|
-
return;
|
|
1380
|
-
}
|
|
1381
|
-
id linked = [_textView.textStorage attribute:JMDEditorLinkAttribute
|
|
1382
|
-
atIndex:wordStart
|
|
1383
|
-
effectiveRange:nil];
|
|
1384
|
-
if (linked != nil) {
|
|
1385
|
-
return;
|
|
1386
|
-
}
|
|
1387
|
-
// Linkify in place: a bare URL re-parses as an autolink in any markdown
|
|
1388
|
-
// renderer, so the editor must show it as a link too (WYSIWYG). The app
|
|
1389
|
-
// can still restyle or remove it from the onLinkDetected callback.
|
|
1390
|
-
const NSRange wordRange = NSMakeRange(wordStart, location - wordStart);
|
|
1391
|
-
// textViewDidChange serializes right after this returns, so no extra
|
|
1392
|
-
// textContentChanged is needed here.
|
|
1393
|
-
if (JMDBlockType([self blockOfLineAt:wordStart]) !=
|
|
1394
|
-
jetmarkdown::EditorBlockType::Code) {
|
|
1395
|
-
[self applyLink:word atomic:NO inRange:wordRange];
|
|
1396
|
-
}
|
|
1397
|
-
if (const auto *emitter = [self editorEventEmitter]) {
|
|
1398
|
-
emitter->onEditorLinkDetected({.url = std::string(word.UTF8String ?: "")});
|
|
1399
|
-
}
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
- (void)applyLink:(NSString *)url atomic:(BOOL)atomic inRange:(NSRange)range {
|
|
1403
|
-
NSTextStorage *storage = _textView.textStorage;
|
|
1404
|
-
[storage beginEditing];
|
|
1405
|
-
[storage enumerateAttributesInRange:range
|
|
1406
|
-
options:0
|
|
1407
|
-
usingBlock:^(NSDictionary *attrs, NSRange runRange, BOOL *stop) {
|
|
1408
|
-
const uint32_t flags =
|
|
1409
|
-
JMDFlagsFromValue(attrs[JMDEditorMarksAttribute]);
|
|
1410
|
-
const uint32_t block =
|
|
1411
|
-
JMDFlagsFromValue(attrs[JMDEditorBlockAttribute]);
|
|
1412
|
-
[storage setAttributes:[self attributesForFlags:flags
|
|
1413
|
-
block:block
|
|
1414
|
-
link:url
|
|
1415
|
-
atomic:atomic]
|
|
1416
|
-
range:runRange];
|
|
1417
|
-
}];
|
|
1418
|
-
[storage endEditing];
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
#pragma mark - UITextViewDelegate
|
|
1422
|
-
|
|
1423
|
-
- (BOOL)textView:(UITextView *)textView
|
|
1424
|
-
shouldChangeTextInRange:(NSRange)range
|
|
1425
|
-
replacementText:(NSString *)text {
|
|
1426
|
-
if (!_multiline && [text containsString:@"\n"]) {
|
|
1427
|
-
[textView resignFirstResponder];
|
|
1428
|
-
return NO;
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
// Normalize CR line endings at the door (system drag-and-drop and some
|
|
1432
|
-
// input methods deliver them); the whole line model assumes "\n".
|
|
1433
|
-
if ([text rangeOfString:@"\r"].location != NSNotFound) {
|
|
1434
|
-
NSString *sanitized =
|
|
1435
|
-
[[text stringByReplacingOccurrencesOfString:@"\r\n" withString:@"\n"]
|
|
1436
|
-
stringByReplacingOccurrencesOfString:@"\r"
|
|
1437
|
-
withString:@"\n"];
|
|
1438
|
-
[_textView.textStorage replaceCharactersInRange:range withString:sanitized];
|
|
1439
|
-
_textView.selectedRange = NSMakeRange(range.location + sanitized.length, 0);
|
|
1440
|
-
[self textViewDidChange:_textView];
|
|
1441
|
-
return NO;
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
// Deleting into an atomic token removes the whole token.
|
|
1445
|
-
if (text.length == 0 && range.length > 0) {
|
|
1446
|
-
NSRange expanded = range;
|
|
1447
|
-
const NSRange headToken = [self atomicRangeAt:range.location];
|
|
1448
|
-
if (headToken.location != NSNotFound) {
|
|
1449
|
-
expanded = NSUnionRange(expanded, headToken);
|
|
1450
|
-
}
|
|
1451
|
-
if (range.length > 1) {
|
|
1452
|
-
const NSRange tailToken = [self atomicRangeAt:NSMaxRange(range) - 1];
|
|
1453
|
-
if (tailToken.location != NSNotFound) {
|
|
1454
|
-
expanded = NSUnionRange(expanded, tailToken);
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
if (!NSEqualRanges(expanded, range)) {
|
|
1458
|
-
[_textView.textStorage replaceCharactersInRange:expanded withString:@""];
|
|
1459
|
-
_textView.selectedRange = NSMakeRange(expanded.location, 0);
|
|
1460
|
-
[self textContentChanged];
|
|
1461
|
-
return NO;
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
// Typing strictly inside an atomic token demotes it to plain text.
|
|
1466
|
-
if (text.length > 0 && range.length == 0 && range.location > 0) {
|
|
1467
|
-
const NSRange token = [self atomicRangeAt:range.location - 1];
|
|
1468
|
-
if (token.location != NSNotFound && range.location > token.location &&
|
|
1469
|
-
range.location < NSMaxRange(token)) {
|
|
1470
|
-
[self applyLink:nil atomic:NO inRange:token];
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
if ([text isEqualToString:@"\n"] && range.length == 0) {
|
|
1475
|
-
const NSRange content = [self contentRangeOfLineAt:range.location];
|
|
1476
|
-
const uint32_t block =
|
|
1477
|
-
content.length > 0 ? [self blockOfLineAt:content.location] : _typingBlock;
|
|
1478
|
-
if (block != 0 && content.length == 0) {
|
|
1479
|
-
// Enter on any empty formatted line (list item, quote, code block,
|
|
1480
|
-
// heading) exits the block instead of continuing it.
|
|
1481
|
-
_typingBlock = 0;
|
|
1482
|
-
[self applyTypingAttributes];
|
|
1483
|
-
[self updateInputTraits];
|
|
1484
|
-
[self invalidateDecorations];
|
|
1485
|
-
[self emitState];
|
|
1486
|
-
return NO;
|
|
1487
|
-
}
|
|
1488
|
-
if (JMDBlockType(block) == jetmarkdown::EditorBlockType::Heading) {
|
|
1489
|
-
// A heading does not continue onto the next line.
|
|
1490
|
-
_paragraphAfterNewline = YES;
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
// A growing word replacement is an autocorrect/QuickType commit; snapshot
|
|
1495
|
-
// the replaced characters' marks so they survive the rebuild (the size
|
|
1496
|
-
// cap keeps select-all replacements out of this path).
|
|
1497
|
-
_pendingMarkRestore = NO;
|
|
1498
|
-
if (range.length > 0 && range.length <= 512 && text.length >= range.length &&
|
|
1499
|
-
NSMaxRange(range) <= _textView.textStorage.length) {
|
|
1500
|
-
_replacedCharFlags.clear();
|
|
1501
|
-
NSTextStorage *storage = _textView.textStorage;
|
|
1502
|
-
for (NSUInteger i = 0; i < range.length; i++) {
|
|
1503
|
-
_replacedCharFlags.push_back(JMDFlagsFromValue(
|
|
1504
|
-
[storage attribute:JMDEditorMarksAttribute
|
|
1505
|
-
atIndex:range.location + i
|
|
1506
|
-
effectiveRange:nil]));
|
|
1507
|
-
}
|
|
1508
|
-
_markRestoreRange = NSMakeRange(range.location, text.length);
|
|
1509
|
-
_pendingMarkRestore = YES;
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
// Backspace at the start of a formatted line clears the block first.
|
|
1513
|
-
if (text.length == 0 && range.length == 1 &&
|
|
1514
|
-
[_textView.text characterAtIndex:range.location] == '\n') {
|
|
1515
|
-
const NSUInteger lineStart = range.location + 1;
|
|
1516
|
-
const uint32_t block = [self blockOfLineAt:lineStart];
|
|
1517
|
-
if (block != 0) {
|
|
1518
|
-
[self applyBlock:0
|
|
1519
|
-
toLinesInRange:NSMakeRange(lineStart, 0)];
|
|
1520
|
-
_typingBlock = 0;
|
|
1521
|
-
[self applyTypingAttributes];
|
|
1522
|
-
[self invalidateDecorations];
|
|
1523
|
-
[self textContentChanged];
|
|
1524
|
-
[self emitState];
|
|
1525
|
-
return NO;
|
|
1526
|
-
}
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
return YES;
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
- (void)textViewDidChange:(UITextView *)textView {
|
|
1533
|
-
if (_pendingMarkRestore) {
|
|
1534
|
-
_pendingMarkRestore = NO;
|
|
1535
|
-
NSTextStorage *storage = _textView.textStorage;
|
|
1536
|
-
const NSUInteger location = _markRestoreRange.location;
|
|
1537
|
-
const NSUInteger count =
|
|
1538
|
-
MIN(_replacedCharFlags.size(), _markRestoreRange.length);
|
|
1539
|
-
if (location + count <= storage.length) {
|
|
1540
|
-
[storage beginEditing];
|
|
1541
|
-
for (NSUInteger i = 0; i < count; i++) {
|
|
1542
|
-
const NSUInteger position = location + i;
|
|
1543
|
-
NSDictionary *attrs = [storage attributesAtIndex:position
|
|
1544
|
-
effectiveRange:nil];
|
|
1545
|
-
const uint32_t existing =
|
|
1546
|
-
JMDFlagsFromValue(attrs[JMDEditorMarksAttribute]);
|
|
1547
|
-
const uint32_t desired = _replacedCharFlags[i];
|
|
1548
|
-
if (existing == desired) {
|
|
1549
|
-
continue;
|
|
1550
|
-
}
|
|
1551
|
-
const uint32_t block = JMDFlagsFromValue(attrs[JMDEditorBlockAttribute]);
|
|
1552
|
-
[storage setAttributes:[self attributesForFlags:desired
|
|
1553
|
-
block:block
|
|
1554
|
-
link:attrs[JMDEditorLinkAttribute]
|
|
1555
|
-
atomic:[attrs[JMDEditorAtomicAttribute]
|
|
1556
|
-
boolValue]]
|
|
1557
|
-
range:NSMakeRange(position, 1)];
|
|
1558
|
-
}
|
|
1559
|
-
[storage endEditing];
|
|
1560
|
-
}
|
|
1561
|
-
_replacedCharFlags.clear();
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
if (_paragraphAfterNewline) {
|
|
1565
|
-
_paragraphAfterNewline = NO;
|
|
1566
|
-
_typingBlock = 0;
|
|
1567
|
-
[self applyTypingAttributes];
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
// A typed newline inherits the previous line's attributes, and TextKit
|
|
1571
|
-
// sizes the trailing empty line fragment from them — after a heading the
|
|
1572
|
-
// caret would stay heading-sized. Normalize the newline: base font and
|
|
1573
|
-
// no marks, keeping the block attr for list/quote continuation (headings
|
|
1574
|
-
// never continue, so theirs is dropped).
|
|
1575
|
-
{
|
|
1576
|
-
const NSRange caret = textView.selectedRange;
|
|
1577
|
-
if (caret.length == 0 && caret.location > 0 &&
|
|
1578
|
-
caret.location <= textView.text.length &&
|
|
1579
|
-
[textView.text characterAtIndex:caret.location - 1] == '\n') {
|
|
1580
|
-
NSTextStorage *storage = textView.textStorage;
|
|
1581
|
-
const NSRange newline = NSMakeRange(caret.location - 1, 1);
|
|
1582
|
-
NSDictionary *attrs = [storage attributesAtIndex:newline.location
|
|
1583
|
-
effectiveRange:nil];
|
|
1584
|
-
uint32_t block = JMDFlagsFromValue(attrs[JMDEditorBlockAttribute]);
|
|
1585
|
-
if (JMDBlockType(block) == jetmarkdown::EditorBlockType::Heading) {
|
|
1586
|
-
block = 0;
|
|
1587
|
-
}
|
|
1588
|
-
[storage setAttributes:[self attributesForFlags:0 block:block]
|
|
1589
|
-
range:newline];
|
|
1590
|
-
}
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
const NSRange selection = textView.selectedRange;
|
|
1594
|
-
if (selection.length == 0 && selection.location > 0 &&
|
|
1595
|
-
selection.location <= textView.text.length) {
|
|
1596
|
-
const unichar last = [textView.text characterAtIndex:selection.location - 1];
|
|
1597
|
-
if (JMDIsWordBreak(last)) {
|
|
1598
|
-
[self detectLinkBefore:selection.location - 1];
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
[self updateMentionSession];
|
|
1602
|
-
[self textContentChanged];
|
|
1603
|
-
// The heading-to-paragraph reset above changes the caret context after
|
|
1604
|
-
// didChangeSelection already emitted; re-emit so toolbars never show a
|
|
1605
|
-
// stale block.
|
|
1606
|
-
[self emitState];
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
- (void)textViewDidChangeSelection:(UITextView *)textView {
|
|
1610
|
-
const NSRange selection = textView.selectedRange;
|
|
1611
|
-
const BOOL moved = !NSEqualRanges(selection, _lastSelection);
|
|
1612
|
-
_lastSelection = selection;
|
|
1613
|
-
if (moved && selection.length == 0) {
|
|
1614
|
-
// Sticky typing state: inherit the marks of the character before the
|
|
1615
|
-
// caret (which is the just-typed character while typing).
|
|
1616
|
-
_typingFlags = [self flagsBeforeCaret:selection.location];
|
|
1617
|
-
const NSRange content = [self contentRangeOfLineAt:selection.location];
|
|
1618
|
-
if (content.length > 0) {
|
|
1619
|
-
_typingBlock = [self blockOfLineAt:content.location];
|
|
1620
|
-
} else if (selection.location > 0 &&
|
|
1621
|
-
selection.location <= _textView.textStorage.length) {
|
|
1622
|
-
// Empty line: inherit the block carried by the preceding newline so
|
|
1623
|
-
// lists continue across Enter.
|
|
1624
|
-
_typingBlock = JMDFlagsFromValue([_textView.textStorage
|
|
1625
|
-
attribute:JMDEditorBlockAttribute
|
|
1626
|
-
atIndex:selection.location - 1
|
|
1627
|
-
effectiveRange:nil]);
|
|
1628
|
-
} else {
|
|
1629
|
-
_typingBlock = 0;
|
|
1630
|
-
}
|
|
1631
|
-
[self applyTypingAttributes];
|
|
1632
|
-
[self updateInputTraits];
|
|
1633
|
-
}
|
|
1634
|
-
if (moved) {
|
|
1635
|
-
[self updateMentionSession];
|
|
1636
|
-
// The empty caret line renders its armed block (marker/stripe), so a
|
|
1637
|
-
// caret move can change what the decorations show.
|
|
1638
|
-
[self invalidateDecorations];
|
|
1639
|
-
}
|
|
1640
|
-
[self emitState];
|
|
1641
|
-
if (moved) {
|
|
1642
|
-
if (const auto *emitter = [self editorEventEmitter]) {
|
|
1643
|
-
emitter->onEditorChangeSelection({
|
|
1644
|
-
.start = static_cast<int>(selection.location),
|
|
1645
|
-
.end = static_cast<int>(selection.location + selection.length),
|
|
1646
|
-
});
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
|
-
- (void)textViewDidBeginEditing:(UITextView *)textView {
|
|
1652
|
-
if (_suppressFocusEvents) {
|
|
1653
|
-
return;
|
|
1654
|
-
}
|
|
1655
|
-
if (const auto *emitter = [self editorEventEmitter]) {
|
|
1656
|
-
emitter->onEditorFocus({});
|
|
1657
|
-
}
|
|
1658
|
-
}
|
|
1659
|
-
|
|
1660
|
-
- (void)textViewDidEndEditing:(UITextView *)textView {
|
|
1661
|
-
if (_suppressFocusEvents) {
|
|
1662
|
-
return;
|
|
1663
|
-
}
|
|
1664
|
-
if (const auto *emitter = [self editorEventEmitter]) {
|
|
1665
|
-
emitter->onEditorBlur({});
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
|
-
#pragma mark - Commands
|
|
1670
|
-
|
|
1671
|
-
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args {
|
|
1672
|
-
RCTJetMarkdownEditorHandleCommand(self, commandName, args);
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
- (void)focus {
|
|
1676
|
-
[_textView becomeFirstResponder];
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
- (void)blur {
|
|
1680
|
-
[_textView resignFirstResponder];
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
- (NSMutableAttributedString *)attributedContentFromMarkdown:(const std::string &)markdown {
|
|
1684
|
-
const auto document = jetmarkdown::editorFromMarkdown(markdown);
|
|
1685
|
-
NSString *text = JMDStringFromCpp(document.text);
|
|
1686
|
-
NSMutableAttributedString *attributed = [[NSMutableAttributedString alloc]
|
|
1687
|
-
initWithString:text
|
|
1688
|
-
attributes:[self attributesForFlags:0 block:0]];
|
|
1689
|
-
|
|
1690
|
-
// Line blocks first (line granularity), then mark runs refine spans.
|
|
1691
|
-
NSUInteger location = 0;
|
|
1692
|
-
size_t lineIndex = 0;
|
|
1693
|
-
while (location <= text.length && lineIndex < document.lines.size()) {
|
|
1694
|
-
NSUInteger start = 0;
|
|
1695
|
-
NSUInteger contentsEnd = 0;
|
|
1696
|
-
NSUInteger end = 0;
|
|
1697
|
-
[text getLineStart:&start
|
|
1698
|
-
end:&end
|
|
1699
|
-
contentsEnd:&contentsEnd
|
|
1700
|
-
forRange:NSMakeRange(MIN(location, text.length), 0)];
|
|
1701
|
-
const auto &line = document.lines[lineIndex];
|
|
1702
|
-
const uint32_t block = JMDPackBlock(line.type, line.level);
|
|
1703
|
-
if (block != 0 && contentsEnd > start) {
|
|
1704
|
-
[attributed setAttributes:[self attributesForFlags:0 block:block]
|
|
1705
|
-
range:NSMakeRange(start, contentsEnd - start)];
|
|
1706
|
-
}
|
|
1707
|
-
if (end <= location || end > text.length) {
|
|
1708
|
-
break;
|
|
1709
|
-
}
|
|
1710
|
-
location = end;
|
|
1711
|
-
lineIndex++;
|
|
1712
|
-
if (contentsEnd == end) {
|
|
1713
|
-
break;
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
for (const auto &run : document.runs) {
|
|
1718
|
-
const NSRange range = NSMakeRange(run.start, run.end - run.start);
|
|
1719
|
-
if (NSMaxRange(range) <= attributed.length) {
|
|
1720
|
-
uint32_t block = 0;
|
|
1721
|
-
if (range.location < attributed.length) {
|
|
1722
|
-
block = JMDFlagsFromValue([attributed attribute:JMDEditorBlockAttribute
|
|
1723
|
-
atIndex:range.location
|
|
1724
|
-
effectiveRange:nil]);
|
|
1725
|
-
}
|
|
1726
|
-
[attributed setAttributes:[self attributesForFlags:run.flags block:block]
|
|
1727
|
-
range:range];
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
for (const auto &link : document.links) {
|
|
1732
|
-
const NSRange range = NSMakeRange(link.start, link.end - link.start);
|
|
1733
|
-
if (NSMaxRange(range) <= attributed.length && range.length > 0) {
|
|
1734
|
-
[attributed enumerateAttributesInRange:range
|
|
1735
|
-
options:0
|
|
1736
|
-
usingBlock:^(NSDictionary *attrs, NSRange runRange, BOOL *stop) {
|
|
1737
|
-
[attributed setAttributes:
|
|
1738
|
-
[self attributesForFlags:JMDFlagsFromValue(
|
|
1739
|
-
attrs[JMDEditorMarksAttribute])
|
|
1740
|
-
block:JMDFlagsFromValue(
|
|
1741
|
-
attrs[JMDEditorBlockAttribute])
|
|
1742
|
-
link:JMDStringFromCpp(link.url)
|
|
1743
|
-
atomic:NO]
|
|
1744
|
-
range:runRange];
|
|
1745
|
-
}];
|
|
1746
|
-
}
|
|
1747
|
-
}
|
|
1748
|
-
|
|
1749
|
-
return attributed;
|
|
1750
|
-
}
|
|
1751
|
-
|
|
1752
|
-
- (void)applyMarkdownValue:(const std::string &)markdown {
|
|
1753
|
-
_textView.attributedText = [self attributedContentFromMarkdown:markdown];
|
|
1754
|
-
_typingFlags = 0;
|
|
1755
|
-
_typingBlock = 0;
|
|
1756
|
-
[self applyTypingAttributes];
|
|
1757
|
-
[self textContentChanged];
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
- (void)setValue:(NSString *)value {
|
|
1761
|
-
[self applyMarkdownValue:std::string(value.UTF8String ?: "")];
|
|
1762
|
-
}
|
|
1763
|
-
|
|
1764
|
-
- (void)insertMarkdown:(NSString *)value {
|
|
1765
|
-
const std::string markdown(value.UTF8String ?: "");
|
|
1766
|
-
if (markdown.empty()) {
|
|
1767
|
-
return;
|
|
1768
|
-
}
|
|
1769
|
-
NSAttributedString *content = [self attributedContentFromMarkdown:markdown];
|
|
1770
|
-
const NSRange selection = _textView.selectedRange;
|
|
1771
|
-
[_textView.textStorage replaceCharactersInRange:selection
|
|
1772
|
-
withAttributedString:content];
|
|
1773
|
-
_textView.selectedRange = NSMakeRange(selection.location + content.length, 0);
|
|
1774
|
-
[self textContentChanged];
|
|
1775
|
-
[self emitState];
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1778
|
-
#pragma mark - JMDEditorTextViewActions
|
|
1779
|
-
|
|
1780
|
-
- (void)editorTextViewDidPaste {
|
|
1781
|
-
UIPasteboard *pasteboard = UIPasteboard.generalPasteboard;
|
|
1782
|
-
const std::string text(pasteboard.string.UTF8String ?: "");
|
|
1783
|
-
|
|
1784
|
-
if (!pasteboard.hasImages) {
|
|
1785
|
-
if (const auto *emitter = [self editorEventEmitter]) {
|
|
1786
|
-
emitter->onEditorPaste({.images = {}, .text = text});
|
|
1787
|
-
}
|
|
1788
|
-
return;
|
|
1789
|
-
}
|
|
1790
|
-
|
|
1791
|
-
// PNG-encoding pasted photos synchronously would freeze the main thread
|
|
1792
|
-
// for the whole encode + disk write; do it off-main and emit when done.
|
|
1793
|
-
NSArray<UIImage *> *pastedImages = pasteboard.images;
|
|
1794
|
-
__weak __typeof(self) weakSelf = self;
|
|
1795
|
-
dispatch_async(
|
|
1796
|
-
dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
|
|
1797
|
-
auto images =
|
|
1798
|
-
std::vector<JetMarkdownEditorEventEmitter::OnEditorPasteImages>();
|
|
1799
|
-
for (UIImage *image in pastedImages) {
|
|
1800
|
-
NSData *data = UIImagePNGRepresentation(image);
|
|
1801
|
-
if (data == nil) {
|
|
1802
|
-
continue;
|
|
1803
|
-
}
|
|
1804
|
-
NSString *path = [NSTemporaryDirectory()
|
|
1805
|
-
stringByAppendingPathComponent:
|
|
1806
|
-
[NSString
|
|
1807
|
-
stringWithFormat:@"jmd-paste-%@.png", NSUUID.UUID.UUIDString]];
|
|
1808
|
-
if (![data writeToFile:path atomically:YES]) {
|
|
1809
|
-
continue;
|
|
1810
|
-
}
|
|
1811
|
-
images.push_back({
|
|
1812
|
-
.height = image.size.height,
|
|
1813
|
-
.url = std::string([NSString stringWithFormat:@"file://%@", path].UTF8String),
|
|
1814
|
-
.width = image.size.width,
|
|
1815
|
-
});
|
|
1816
|
-
}
|
|
1817
|
-
auto shared =
|
|
1818
|
-
std::make_shared<std::vector<JetMarkdownEditorEventEmitter::OnEditorPasteImages>>(
|
|
1819
|
-
std::move(images));
|
|
1820
|
-
dispatch_async(dispatch_get_main_queue(), ^{
|
|
1821
|
-
__typeof(self) strongSelf = weakSelf;
|
|
1822
|
-
if (strongSelf == nil) {
|
|
1823
|
-
return;
|
|
1824
|
-
}
|
|
1825
|
-
if (const auto *emitter = [strongSelf editorEventEmitter]) {
|
|
1826
|
-
emitter->onEditorPaste({.images = std::move(*shared), .text = text});
|
|
1827
|
-
}
|
|
1828
|
-
});
|
|
1829
|
-
});
|
|
1830
|
-
}
|
|
1831
|
-
|
|
1832
|
-
- (void)editorTextViewShortcut:(uint32_t)mark {
|
|
1833
|
-
[self toggleMark:mark];
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
|
-
- (BOOL)editorTextViewHandleDeleteAtDocumentStart {
|
|
1837
|
-
const uint32_t block = [self blockOfLineAt:0] ?: _typingBlock;
|
|
1838
|
-
if (block == 0) {
|
|
1839
|
-
return NO;
|
|
1840
|
-
}
|
|
1841
|
-
[self applyBlock:0 toLinesInRange:NSMakeRange(0, 0)];
|
|
1842
|
-
_typingBlock = 0;
|
|
1843
|
-
[self applyTypingAttributes];
|
|
1844
|
-
[self updateInputTraits];
|
|
1845
|
-
[self invalidateDecorations];
|
|
1846
|
-
[self textContentChanged];
|
|
1847
|
-
[self emitState];
|
|
1848
|
-
return YES;
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
- (void)setSelection:(NSInteger)start end:(NSInteger)end {
|
|
1852
|
-
const NSInteger length = (NSInteger)_textView.text.length;
|
|
1853
|
-
const NSInteger clampedStart = MAX(0, MIN(start, length));
|
|
1854
|
-
const NSInteger clampedEnd = MAX(clampedStart, MIN(end, length));
|
|
1855
|
-
_textView.selectedRange = NSMakeRange(clampedStart, clampedEnd - clampedStart);
|
|
1856
|
-
}
|
|
1857
|
-
|
|
1858
|
-
- (void)toggleBold {
|
|
1859
|
-
[self toggleMark:jetmarkdown::MarkBold];
|
|
1860
|
-
}
|
|
1861
|
-
|
|
1862
|
-
- (void)toggleCode {
|
|
1863
|
-
[self toggleMark:jetmarkdown::MarkInlineCode];
|
|
1864
|
-
}
|
|
1865
|
-
|
|
1866
|
-
- (void)toggleItalic {
|
|
1867
|
-
[self toggleMark:jetmarkdown::MarkItalic];
|
|
1868
|
-
}
|
|
1869
|
-
|
|
1870
|
-
- (void)toggleSpoiler {
|
|
1871
|
-
[self toggleMark:jetmarkdown::MarkSpoiler];
|
|
1872
|
-
}
|
|
1873
|
-
|
|
1874
|
-
- (void)toggleStrikethrough {
|
|
1875
|
-
[self toggleMark:jetmarkdown::MarkStrikethrough];
|
|
1876
|
-
}
|
|
1877
|
-
|
|
1878
|
-
- (void)toggleSubscript {
|
|
1879
|
-
[self toggleMark:jetmarkdown::MarkSubscript];
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
- (void)toggleSuperscript {
|
|
1883
|
-
[self toggleMark:jetmarkdown::MarkSuperscript];
|
|
1884
|
-
}
|
|
1885
|
-
|
|
1886
|
-
- (void)toggleBlockQuote {
|
|
1887
|
-
[self toggleBlock:jetmarkdown::EditorBlockType::Quote level:0];
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
|
-
- (void)toggleCodeBlock {
|
|
1891
|
-
[self toggleBlock:jetmarkdown::EditorBlockType::Code level:0];
|
|
1892
|
-
}
|
|
1893
|
-
|
|
1894
|
-
- (void)toggleHeading:(NSInteger)level {
|
|
1895
|
-
[self toggleBlock:jetmarkdown::EditorBlockType::Heading
|
|
1896
|
-
level:(uint8_t)MAX(1, MIN(level, 6))];
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
- (void)toggleOrderedList {
|
|
1900
|
-
[self toggleBlock:jetmarkdown::EditorBlockType::Ordered level:0];
|
|
1901
|
-
}
|
|
1902
|
-
|
|
1903
|
-
- (void)toggleUnorderedList {
|
|
1904
|
-
[self toggleBlock:jetmarkdown::EditorBlockType::Bullet level:0];
|
|
1905
|
-
}
|
|
1906
|
-
|
|
1907
|
-
- (NSString *)singleLine:(NSString *)value {
|
|
1908
|
-
NSString *flattened =
|
|
1909
|
-
[[value stringByReplacingOccurrencesOfString:@"\r" withString:@" "]
|
|
1910
|
-
stringByReplacingOccurrencesOfString:@"\n"
|
|
1911
|
-
withString:@" "];
|
|
1912
|
-
return flattened;
|
|
1913
|
-
}
|
|
1914
|
-
|
|
1915
|
-
- (void)insertLink:(NSString *)url label:(NSString *)label {
|
|
1916
|
-
if (url.length == 0) {
|
|
1917
|
-
return;
|
|
1918
|
-
}
|
|
1919
|
-
label = [self singleLine:label ?: @""];
|
|
1920
|
-
const NSRange selection = _textView.selectedRange;
|
|
1921
|
-
// A code fence carries raw text only — a link there would render in the
|
|
1922
|
-
// editor but vanish from the markdown.
|
|
1923
|
-
if ([self selectionTouchesCodeBlock:selection]) {
|
|
1924
|
-
return;
|
|
1925
|
-
}
|
|
1926
|
-
if (selection.length > 0) {
|
|
1927
|
-
[self applyLink:url atomic:NO inRange:selection];
|
|
1928
|
-
_textView.selectedRange = NSMakeRange(NSMaxRange(selection), 0);
|
|
1929
|
-
} else {
|
|
1930
|
-
NSString *content = label.length > 0 ? label : url;
|
|
1931
|
-
NSAttributedString *linked = [[NSAttributedString alloc]
|
|
1932
|
-
initWithString:content
|
|
1933
|
-
attributes:[self attributesForFlags:_typingFlags
|
|
1934
|
-
block:_typingBlock
|
|
1935
|
-
link:url
|
|
1936
|
-
atomic:NO]];
|
|
1937
|
-
[_textView.textStorage insertAttributedString:linked
|
|
1938
|
-
atIndex:selection.location];
|
|
1939
|
-
_textView.selectedRange = NSMakeRange(selection.location + content.length, 0);
|
|
1940
|
-
}
|
|
1941
|
-
[self applyTypingAttributes];
|
|
1942
|
-
[self textContentChanged];
|
|
1943
|
-
}
|
|
1944
|
-
|
|
1945
|
-
- (void)removeLink {
|
|
1946
|
-
const NSRange selection = _textView.selectedRange;
|
|
1947
|
-
NSRange target = selection;
|
|
1948
|
-
if (selection.length == 0) {
|
|
1949
|
-
NSTextStorage *storage = _textView.textStorage;
|
|
1950
|
-
const NSUInteger probe =
|
|
1951
|
-
selection.location > 0 ? selection.location - 1 : 0;
|
|
1952
|
-
if (storage.length == 0 || probe >= storage.length) {
|
|
1953
|
-
return;
|
|
1954
|
-
}
|
|
1955
|
-
NSRange effective = NSMakeRange(NSNotFound, 0);
|
|
1956
|
-
id value = [storage attribute:JMDEditorLinkAttribute
|
|
1957
|
-
atIndex:probe
|
|
1958
|
-
longestEffectiveRange:&effective
|
|
1959
|
-
inRange:NSMakeRange(0, storage.length)];
|
|
1960
|
-
if (value == nil) {
|
|
1961
|
-
return;
|
|
1962
|
-
}
|
|
1963
|
-
target = effective;
|
|
1964
|
-
}
|
|
1965
|
-
[self applyLink:nil atomic:NO inRange:target];
|
|
1966
|
-
[self textContentChanged];
|
|
1967
|
-
}
|
|
1968
|
-
|
|
1969
|
-
- (void)insertMention:(NSString *)trigger label:(NSString *)label url:(NSString *)url {
|
|
1970
|
-
if (label.length == 0 || url.length == 0) {
|
|
1971
|
-
return;
|
|
1972
|
-
}
|
|
1973
|
-
label = [self singleLine:label];
|
|
1974
|
-
if ([self selectionTouchesCodeBlock:_textView.selectedRange]) {
|
|
1975
|
-
return;
|
|
1976
|
-
}
|
|
1977
|
-
// Replaces the active mention query (trigger included), or inserts at
|
|
1978
|
-
// the caret. A trailing space keeps typing outside the token.
|
|
1979
|
-
NSRange target = _textView.selectedRange;
|
|
1980
|
-
if (_mentionActive) {
|
|
1981
|
-
target = NSMakeRange(_mentionStart, target.location - _mentionStart);
|
|
1982
|
-
}
|
|
1983
|
-
NSString *token = [NSString stringWithFormat:@"%@%@", trigger ?: @"", label];
|
|
1984
|
-
NSMutableAttributedString *inserted = [[NSMutableAttributedString alloc]
|
|
1985
|
-
initWithString:token
|
|
1986
|
-
attributes:[self attributesForFlags:0
|
|
1987
|
-
block:_typingBlock
|
|
1988
|
-
link:url
|
|
1989
|
-
atomic:YES]];
|
|
1990
|
-
[inserted appendAttributedString:[[NSAttributedString alloc]
|
|
1991
|
-
initWithString:@" "
|
|
1992
|
-
attributes:[self attributesForFlags:0 block:_typingBlock]]];
|
|
1993
|
-
[_textView.textStorage replaceCharactersInRange:target
|
|
1994
|
-
withAttributedString:inserted];
|
|
1995
|
-
_textView.selectedRange = NSMakeRange(target.location + inserted.length, 0);
|
|
1996
|
-
[self endMentionSession];
|
|
1997
|
-
_typingFlags = 0;
|
|
1998
|
-
[self applyTypingAttributes];
|
|
1999
|
-
[self textContentChanged];
|
|
2000
|
-
}
|
|
2001
|
-
|
|
2002
|
-
@end
|