react-native-enriched 0.1.5 → 0.2.0
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 +3 -9
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerDelegate.java +4 -1
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerInterface.java +2 -1
- package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.cpp +5 -0
- package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.h +1 -45
- package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt +53 -12
- package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewLayoutManager.kt +7 -56
- package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt +19 -22
- package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewPackage.kt +2 -0
- package/android/src/main/java/com/swmansion/enriched/MeasurementStore.kt +158 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedCodeBlockSpan.kt +36 -1
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedImageSpan.kt +132 -11
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt +65 -46
- package/android/src/main/java/com/swmansion/enriched/spans/utils/ForceRedrawSpan.kt +13 -0
- package/android/src/main/java/com/swmansion/enriched/styles/HtmlStyle.kt +2 -9
- package/android/src/main/java/com/swmansion/enriched/styles/InlineStyles.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/styles/ParagraphStyles.kt +110 -3
- package/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt +75 -32
- package/android/src/main/java/com/swmansion/enriched/utils/AsyncDrawable.kt +91 -0
- package/android/src/main/java/com/swmansion/enriched/utils/EnrichedParser.java +38 -15
- package/android/src/main/java/com/swmansion/enriched/utils/ResourceManager.kt +26 -0
- package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedSpanWatcher.kt +3 -1
- package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedTextWatcher.kt +1 -1
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.cpp +15 -2
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.h +1 -0
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputShadowNode.cpp +1 -2
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/conversions.h +27 -0
- package/android/src/main/res/drawable/broken_image.xml +10 -0
- package/ios/EnrichedTextInputView.h +3 -1
- package/ios/EnrichedTextInputView.mm +167 -68
- package/ios/config/InputConfig.h +6 -0
- package/ios/config/InputConfig.mm +32 -0
- package/ios/generated/RNEnrichedTextInputViewSpec/Props.cpp +5 -0
- package/ios/generated/RNEnrichedTextInputViewSpec/Props.h +1 -45
- package/ios/generated/RNEnrichedTextInputViewSpec/RCTComponentViewHelpers.h +20 -4
- package/ios/inputParser/InputParser.mm +179 -31
- package/ios/inputTextView/InputTextView.mm +3 -5
- package/ios/internals/EnrichedTextInputViewShadowNode.h +1 -0
- package/ios/internals/EnrichedTextInputViewShadowNode.mm +29 -17
- package/ios/styles/BlockQuoteStyle.mm +5 -26
- package/ios/styles/BoldStyle.mm +2 -0
- package/ios/styles/CodeBlockStyle.mm +228 -0
- package/ios/styles/H1Style.mm +1 -0
- package/ios/styles/H2Style.mm +1 -0
- package/ios/styles/H3Style.mm +1 -0
- package/ios/styles/ImageStyle.mm +158 -0
- package/ios/styles/InlineCodeStyle.mm +2 -0
- package/ios/styles/ItalicStyle.mm +2 -0
- package/ios/styles/LinkStyle.mm +15 -7
- package/ios/styles/MentionStyle.mm +133 -36
- package/ios/styles/OrderedListStyle.mm +5 -8
- package/ios/styles/StrikethroughStyle.mm +2 -0
- package/ios/styles/UnderlineStyle.mm +2 -0
- package/ios/styles/UnorderedListStyle.mm +5 -8
- package/ios/utils/BaseStyleProtocol.h +1 -0
- package/ios/utils/ImageData.h +10 -0
- package/ios/utils/ImageData.mm +4 -0
- package/ios/utils/LayoutManagerExtension.mm +118 -3
- package/ios/utils/OccurenceUtils.h +4 -0
- package/ios/utils/OccurenceUtils.mm +47 -0
- package/ios/utils/ParagraphAttributesUtils.h +1 -0
- package/ios/utils/ParagraphAttributesUtils.mm +87 -20
- package/ios/utils/StringExtension.h +1 -1
- package/ios/utils/StringExtension.mm +17 -8
- package/ios/utils/StyleHeaders.h +12 -0
- package/ios/utils/ZeroWidthSpaceUtils.mm +22 -10
- package/lib/module/EnrichedTextInput.js +4 -2
- package/lib/module/EnrichedTextInput.js.map +1 -1
- package/lib/module/EnrichedTextInputNativeComponent.ts +7 -5
- package/lib/module/normalizeHtmlStyle.js +0 -4
- package/lib/module/normalizeHtmlStyle.js.map +1 -1
- package/lib/typescript/src/EnrichedTextInput.d.ts +3 -6
- package/lib/typescript/src/EnrichedTextInput.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts +2 -5
- package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/normalizeHtmlStyle.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/EnrichedTextInput.tsx +6 -7
- package/src/EnrichedTextInputNativeComponent.ts +7 -5
- package/src/normalizeHtmlStyle.ts +0 -4
|
@@ -18,6 +18,8 @@ static NSString *const MentionAttributeName = @"MentionAttributeName";
|
|
|
18
18
|
|
|
19
19
|
+ (StyleType)getStyleType { return Mention; }
|
|
20
20
|
|
|
21
|
+
+ (BOOL)isParagraphStyle { return NO; }
|
|
22
|
+
|
|
21
23
|
- (instancetype)initWithInput:(id)input {
|
|
22
24
|
self = [super init];
|
|
23
25
|
_input = (EnrichedTextInputView *)input;
|
|
@@ -297,52 +299,37 @@ static NSString *const MentionAttributeName = @"MentionAttributeName";
|
|
|
297
299
|
return;
|
|
298
300
|
}
|
|
299
301
|
|
|
300
|
-
// get the
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if(currentWord == nullptr) {
|
|
304
|
-
[self removeActiveMentionRange];
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// get word properties
|
|
309
|
-
NSString *wordText = (NSString *)[currentWord objectForKey:@"word"];
|
|
310
|
-
NSValue *wordRangeValue = (NSValue *)[currentWord objectForKey:@"range"];
|
|
311
|
-
if(wordText == nullptr || wordRangeValue == nullptr) {
|
|
302
|
+
// get the text (and its range) that could be an editable mention
|
|
303
|
+
NSArray *mentionCandidate = [self getMentionCandidate];
|
|
304
|
+
if(mentionCandidate == nullptr) {
|
|
312
305
|
[self removeActiveMentionRange];
|
|
313
306
|
return;
|
|
314
307
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
[
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
// get conflicting style classes
|
|
331
|
-
LinkStyle* linkStyle = [_input->stylesDict objectForKey:@([LinkStyle getStyleType])];
|
|
332
|
-
InlineCodeStyle* inlineCodeStyle = [_input->stylesDict objectForKey:@([InlineCodeStyle getStyleType])];
|
|
333
|
-
if(linkStyle == nullptr || inlineCodeStyle == nullptr) {
|
|
334
|
-
[self removeActiveMentionRange];
|
|
335
|
-
return;
|
|
308
|
+
NSString *candidateText = mentionCandidate[0];
|
|
309
|
+
NSRange candidateRange = [(NSValue *)mentionCandidate[1] rangeValue];
|
|
310
|
+
|
|
311
|
+
// get style classes that the mention shouldn't be recognized in, together with other mentions
|
|
312
|
+
NSArray *conflicts = _input->conflictingStyles[@([MentionStyle getStyleType])];
|
|
313
|
+
NSArray *blocks = _input->blockingStyles[@([MentionStyle getStyleType])];
|
|
314
|
+
NSArray *allConflicts = [[conflicts arrayByAddingObjectsFromArray:blocks] arrayByAddingObject:@([MentionStyle getStyleType])];
|
|
315
|
+
BOOL conflictingStyle = NO;
|
|
316
|
+
|
|
317
|
+
for(NSNumber *styleType in allConflicts) {
|
|
318
|
+
id<BaseStyleProtocol> styleClass = _input->stylesDict[styleType];
|
|
319
|
+
if(styleClass != nullptr && [styleClass anyOccurence:candidateRange]) {
|
|
320
|
+
conflictingStyle = YES;
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
336
323
|
}
|
|
337
324
|
|
|
338
|
-
// if
|
|
339
|
-
if(
|
|
325
|
+
// if any of the conflicting styles were present, don't edit the mention
|
|
326
|
+
if(conflictingStyle) {
|
|
340
327
|
[self removeActiveMentionRange];
|
|
341
328
|
return;
|
|
342
329
|
}
|
|
343
330
|
|
|
344
331
|
// everything checks out - we are indeed editing a mention
|
|
345
|
-
[self setActiveMentionRange:
|
|
332
|
+
[self setActiveMentionRange:candidateRange text:candidateText];
|
|
346
333
|
}
|
|
347
334
|
|
|
348
335
|
// used to fix mentions' typing attributes
|
|
@@ -454,6 +441,116 @@ static NSString *const MentionAttributeName = @"MentionAttributeName";
|
|
|
454
441
|
return [_input->config mentionStylePropsForIndicator:params.indicator];
|
|
455
442
|
}
|
|
456
443
|
|
|
444
|
+
// finds if any word/words around current selection are eligible to be edited as mentions
|
|
445
|
+
// since we allow for a single space inside an edited mention, we have take both current and the previous word into account
|
|
446
|
+
- (NSArray *)getMentionCandidate {
|
|
447
|
+
NSDictionary *currentWord, *previousWord;
|
|
448
|
+
NSString *currentWordText, *previousWordText, *finalText;
|
|
449
|
+
NSValue *currentWordRange, *previousWordRange;
|
|
450
|
+
NSRange finalRange;
|
|
451
|
+
|
|
452
|
+
// word at the current selection
|
|
453
|
+
currentWord = [WordsUtils getCurrentWord:_input->textView.textStorage.string range:_input->textView.selectedRange];
|
|
454
|
+
if(currentWord != nullptr ) {
|
|
455
|
+
currentWordText = (NSString *)[currentWord objectForKey:@"word"];
|
|
456
|
+
currentWordRange = (NSValue *)[currentWord objectForKey:@"range"];
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if(currentWord != nullptr) {
|
|
460
|
+
// current word exists
|
|
461
|
+
unichar currentFirstChar = [currentWordText characterAtIndex:0];
|
|
462
|
+
|
|
463
|
+
if([[_input->config mentionIndicators] containsObject:@(currentFirstChar)]) {
|
|
464
|
+
// current word exists and has a mention indicator; no need to check for the previous word
|
|
465
|
+
finalText = currentWordText;
|
|
466
|
+
finalRange = [currentWordRange rangeValue];
|
|
467
|
+
} else {
|
|
468
|
+
// current word exists but no traces of mention indicator; get the previous word
|
|
469
|
+
|
|
470
|
+
NSInteger previousWordSearchLocation = [currentWordRange rangeValue].location - 1;
|
|
471
|
+
if(previousWordSearchLocation < 0) {
|
|
472
|
+
// previous word can't exist
|
|
473
|
+
return nullptr;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
unichar separatorChar = [_input->textView.textStorage.string characterAtIndex:previousWordSearchLocation];
|
|
477
|
+
if(![[NSCharacterSet whitespaceCharacterSet] characterIsMember:separatorChar]) {
|
|
478
|
+
// we want to check for the previous word ONLY if the separating character was a space
|
|
479
|
+
// newlines don't make it
|
|
480
|
+
return nullptr;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
previousWord = [WordsUtils getCurrentWord:_input->textView.textStorage.string range:NSMakeRange(previousWordSearchLocation, 0)];
|
|
484
|
+
|
|
485
|
+
if(previousWord != nullptr) {
|
|
486
|
+
// previous word exists; get its properties
|
|
487
|
+
previousWordText = (NSString *)[previousWord objectForKey:@"word"];
|
|
488
|
+
previousWordRange = (NSValue *)[previousWord objectForKey:@"range"];
|
|
489
|
+
|
|
490
|
+
// check for the mention indicators in the previous word
|
|
491
|
+
unichar previousFirstChar = [previousWordText characterAtIndex:0];
|
|
492
|
+
|
|
493
|
+
if([[_input->config mentionIndicators] containsObject:@(previousFirstChar) ]) {
|
|
494
|
+
// previous word has a proper mention indicator: treat both words as an editable mention
|
|
495
|
+
finalText = [NSString stringWithFormat:@"%@ %@", previousWordText, currentWordText];
|
|
496
|
+
// range length is both words' lengths + 1 for a space between them
|
|
497
|
+
finalRange = NSMakeRange(
|
|
498
|
+
[previousWordRange rangeValue].location,
|
|
499
|
+
[previousWordRange rangeValue].length + [currentWordRange rangeValue].length + 1
|
|
500
|
+
);
|
|
501
|
+
} else {
|
|
502
|
+
// neither current nor previous words have a mention indicator
|
|
503
|
+
return nullptr;
|
|
504
|
+
}
|
|
505
|
+
} else {
|
|
506
|
+
// previous word doesn't exist and no mention indicators in the current word
|
|
507
|
+
return nullptr;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
} else {
|
|
511
|
+
// current word doesn't exist; try getting the previous one
|
|
512
|
+
|
|
513
|
+
NSInteger previousWordSearchLocation = _input->textView.selectedRange.location - 1;
|
|
514
|
+
if(previousWordSearchLocation < 0) {
|
|
515
|
+
// previous word can't exist
|
|
516
|
+
return nullptr;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
unichar separatorChar = [_input->textView.textStorage.string characterAtIndex:previousWordSearchLocation];
|
|
520
|
+
if(![[NSCharacterSet whitespaceCharacterSet] characterIsMember:separatorChar]) {
|
|
521
|
+
// we want to check for the previous word ONLY if the separating character was a space
|
|
522
|
+
// newlines don't make it
|
|
523
|
+
return nullptr;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
previousWord = [WordsUtils getCurrentWord:_input->textView.textStorage.string range:NSMakeRange(previousWordSearchLocation, 0)];
|
|
527
|
+
|
|
528
|
+
if(previousWord != nullptr) {
|
|
529
|
+
// previous word exists; get its properties
|
|
530
|
+
previousWordText = (NSString *)[previousWord objectForKey:@"word"];
|
|
531
|
+
previousWordRange = (NSValue *)[previousWord objectForKey:@"range"];
|
|
532
|
+
|
|
533
|
+
// check for the mention indicators in the previous word
|
|
534
|
+
unichar previousFirstChar = [previousWordText characterAtIndex:0];
|
|
535
|
+
|
|
536
|
+
if([[_input->config mentionIndicators] containsObject:@(previousFirstChar)]) {
|
|
537
|
+
// previous word has a proper mention indicator; treat previous word + a space as a editable mention
|
|
538
|
+
finalText = [NSString stringWithFormat:@"%@ ", previousWordText];
|
|
539
|
+
// the range length is previous word length + 1 for a space
|
|
540
|
+
finalRange = NSMakeRange([previousWordRange rangeValue].location, [previousWordRange rangeValue].length + 1);
|
|
541
|
+
} else {
|
|
542
|
+
// no current word, previous has no mention indicators
|
|
543
|
+
return nullptr;
|
|
544
|
+
}
|
|
545
|
+
} else {
|
|
546
|
+
// no current word, no previous word
|
|
547
|
+
return nullptr;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return @[finalText, [NSValue valueWithRange:finalRange]];
|
|
552
|
+
}
|
|
553
|
+
|
|
457
554
|
// both used for setting the active mention range + indicator and fires proper onMention event
|
|
458
555
|
- (void)setActiveMentionRange:(NSRange)range text:(NSString *)text {
|
|
459
556
|
NSString *indicatorString = [NSString stringWithFormat:@"%C", [text characterAtIndex:0]];
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
+ (StyleType)getStyleType { return OrderedList; }
|
|
13
13
|
|
|
14
|
+
+ (BOOL)isParagraphStyle { return YES; }
|
|
15
|
+
|
|
14
16
|
- (CGFloat)getHeadIndent {
|
|
15
17
|
// lists are drawn manually
|
|
16
18
|
// margin before marker + gap between marker and paragraph
|
|
@@ -158,18 +160,13 @@
|
|
|
158
160
|
if(charBefore == '1') {
|
|
159
161
|
// we got a match - add a list if possible
|
|
160
162
|
if([_input handleStyleBlocksAndConflicts:[[self class] getStyleType] range:paragraphRange]) {
|
|
161
|
-
// don't emit
|
|
162
|
-
|
|
163
|
-
if(prevEmitHtml) {
|
|
164
|
-
_input->emitHtml = NO;
|
|
165
|
-
}
|
|
163
|
+
// don't emit during the replacing
|
|
164
|
+
_input->blockEmitting = YES;
|
|
166
165
|
|
|
167
166
|
// remove the number
|
|
168
167
|
[TextInsertionUtils replaceText:@"" at:NSMakeRange(paragraphRange.location, 1) additionalAttributes:nullptr input:_input withSelection:YES];
|
|
169
168
|
|
|
170
|
-
|
|
171
|
-
_input->emitHtml = YES;
|
|
172
|
-
}
|
|
169
|
+
_input->blockEmitting = NO;
|
|
173
170
|
|
|
174
171
|
// add attributes on the paragraph
|
|
175
172
|
[self addAttributes:NSMakeRange(paragraphRange.location, paragraphRange.length - 1)];
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
+ (StyleType)getStyleType { return UnorderedList; }
|
|
13
13
|
|
|
14
|
+
+ (BOOL)isParagraphStyle { return YES; }
|
|
15
|
+
|
|
14
16
|
- (CGFloat)getHeadIndent {
|
|
15
17
|
// lists are drawn manually
|
|
16
18
|
// margin before bullet + gap between bullet and paragraph
|
|
@@ -158,18 +160,13 @@
|
|
|
158
160
|
if(charBefore == '-') {
|
|
159
161
|
// we got a match - add a list if possible
|
|
160
162
|
if([_input handleStyleBlocksAndConflicts:[[self class] getStyleType] range:paragraphRange]) {
|
|
161
|
-
// don't emit
|
|
162
|
-
|
|
163
|
-
if(prevEmitHtml) {
|
|
164
|
-
_input->emitHtml = NO;
|
|
165
|
-
}
|
|
163
|
+
// don't emit during the replacing
|
|
164
|
+
_input->blockEmitting = YES;
|
|
166
165
|
|
|
167
166
|
// remove the dash
|
|
168
167
|
[TextInsertionUtils replaceText:@"" at:NSMakeRange(paragraphRange.location, 1) additionalAttributes:nullptr input:_input withSelection:YES];
|
|
169
168
|
|
|
170
|
-
|
|
171
|
-
_input->emitHtml = YES;
|
|
172
|
-
}
|
|
169
|
+
_input->blockEmitting = NO;
|
|
173
170
|
|
|
174
171
|
// add attributes on the dashless paragraph
|
|
175
172
|
[self addAttributes:NSMakeRange(paragraphRange.location, paragraphRange.length - 1)];
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
#import "EnrichedTextInputView.h"
|
|
4
4
|
#import "StyleHeaders.h"
|
|
5
5
|
#import "ParagraphsUtils.h"
|
|
6
|
+
#import "ColorExtension.h"
|
|
6
7
|
|
|
7
8
|
@implementation NSLayoutManager (LayoutManagerExtension)
|
|
8
9
|
|
|
@@ -52,11 +53,122 @@ static void const *kInputKey = &kInputKey;
|
|
|
52
53
|
EnrichedTextInputView *typedInput = (EnrichedTextInputView *)self.input;
|
|
53
54
|
if(typedInput == nullptr) { return; }
|
|
54
55
|
|
|
56
|
+
NSRange inputRange = NSMakeRange(0, typedInput->textView.textStorage.length);
|
|
57
|
+
|
|
58
|
+
[self drawBlockQuotes:typedInput origin:origin inputRange:inputRange];
|
|
59
|
+
[self drawLists:typedInput origin:origin inputRange:inputRange];
|
|
60
|
+
[self drawCodeBlocks:typedInput origin:origin inputRange:inputRange];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
- (void)drawCodeBlocks:(EnrichedTextInputView *)typedInput origin:(CGPoint)origin inputRange:(NSRange)inputRange
|
|
64
|
+
{
|
|
65
|
+
CodeBlockStyle *codeBlockStyle = typedInput->stylesDict[@([CodeBlockStyle getStyleType])];
|
|
66
|
+
if(codeBlockStyle == nullptr) { return; }
|
|
67
|
+
|
|
68
|
+
NSArray<StylePair *> *allCodeBlocks = [codeBlockStyle findAllOccurences:inputRange];
|
|
69
|
+
NSArray<StylePair *> *mergedCodeBlocks = [self mergeContiguousStylePairs:allCodeBlocks];
|
|
70
|
+
UIColor *bgColor = [[typedInput->config codeBlockBgColor] colorWithAlphaIfNotTransparent:0.4];
|
|
71
|
+
CGFloat radius = [typedInput->config codeBlockBorderRadius];
|
|
72
|
+
[bgColor setFill];
|
|
73
|
+
|
|
74
|
+
for (StylePair *pair in mergedCodeBlocks) {
|
|
75
|
+
NSRange blockCharacterRange = [pair.rangeValue rangeValue];
|
|
76
|
+
if (blockCharacterRange.length == 0) continue;
|
|
77
|
+
|
|
78
|
+
NSArray *paragraphs = [ParagraphsUtils getSeparateParagraphsRangesIn:typedInput->textView range:blockCharacterRange];
|
|
79
|
+
if (paragraphs.count == 0) continue;
|
|
80
|
+
|
|
81
|
+
NSRange firstParagraphRange = [((NSValue *)[paragraphs firstObject]) rangeValue];
|
|
82
|
+
NSRange lastParagraphRange = [((NSValue *)[paragraphs lastObject]) rangeValue];
|
|
83
|
+
|
|
84
|
+
for (NSValue *paragraphValue in paragraphs) {
|
|
85
|
+
NSRange paragraphCharacterRange = [paragraphValue rangeValue];
|
|
86
|
+
|
|
87
|
+
BOOL isFirstParagraph = NSEqualRanges(paragraphCharacterRange, firstParagraphRange);
|
|
88
|
+
BOOL isLastParagraph = NSEqualRanges(paragraphCharacterRange, lastParagraphRange);
|
|
89
|
+
|
|
90
|
+
NSRange paragraphGlyphRange = [self glyphRangeForCharacterRange:paragraphCharacterRange actualCharacterRange:NULL];
|
|
91
|
+
|
|
92
|
+
__block BOOL isFirstLineOfParagraph = YES;
|
|
93
|
+
|
|
94
|
+
[self enumerateLineFragmentsForGlyphRange:paragraphGlyphRange
|
|
95
|
+
usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer * _Nonnull textContainer, NSRange glyphRange, BOOL * _Nonnull stop) {
|
|
96
|
+
|
|
97
|
+
CGRect lineBgRect = rect;
|
|
98
|
+
lineBgRect.origin.x = origin.x;
|
|
99
|
+
lineBgRect.origin.y += origin.y;
|
|
100
|
+
lineBgRect.size.width = textContainer.size.width;
|
|
101
|
+
|
|
102
|
+
UIRectCorner cornersForThisLine = 0;
|
|
103
|
+
|
|
104
|
+
if (isFirstParagraph && isFirstLineOfParagraph) {
|
|
105
|
+
cornersForThisLine = UIRectCornerTopLeft | UIRectCornerTopRight;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
BOOL isLastLineOfParagraph = (NSMaxRange(glyphRange) >= NSMaxRange(paragraphGlyphRange));
|
|
109
|
+
|
|
110
|
+
if (isLastParagraph && isLastLineOfParagraph) {
|
|
111
|
+
cornersForThisLine = cornersForThisLine | UIRectCornerBottomLeft | UIRectCornerBottomRight;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:lineBgRect
|
|
115
|
+
byRoundingCorners:cornersForThisLine
|
|
116
|
+
cornerRadii:CGSizeMake(radius, radius)];
|
|
117
|
+
[path fill];
|
|
118
|
+
|
|
119
|
+
isFirstLineOfParagraph = NO;
|
|
120
|
+
}];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
- (NSArray<StylePair *> *)mergeContiguousStylePairs:(NSArray<StylePair *> *)pairs
|
|
126
|
+
{
|
|
127
|
+
if (pairs.count == 0) {
|
|
128
|
+
return @[];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
NSMutableArray<StylePair *> *mergedPairs = [[NSMutableArray alloc] init];
|
|
132
|
+
StylePair *currentPair = pairs[0];
|
|
133
|
+
NSRange currentRange = [currentPair.rangeValue rangeValue];
|
|
134
|
+
for (NSUInteger i = 1; i < pairs.count; i++) {
|
|
135
|
+
StylePair *nextPair = pairs[i];
|
|
136
|
+
NSRange nextRange = [nextPair.rangeValue rangeValue];
|
|
137
|
+
|
|
138
|
+
// The Gap Check:
|
|
139
|
+
// NSMaxRange(currentRange) is where the current block ends.
|
|
140
|
+
// nextRange.location is where the next block starts.
|
|
141
|
+
if (NSMaxRange(currentRange) == nextRange.location) {
|
|
142
|
+
// They touch perfectly (no gap). Merge them.
|
|
143
|
+
currentRange.length += nextRange.length;
|
|
144
|
+
} else {
|
|
145
|
+
// There is a gap (indices don't match).
|
|
146
|
+
// 1. Save the finished block.
|
|
147
|
+
StylePair *mergedPair = [[StylePair alloc] init];
|
|
148
|
+
mergedPair.rangeValue = [NSValue valueWithRange:currentRange];
|
|
149
|
+
mergedPair.styleValue = currentPair.styleValue;
|
|
150
|
+
[mergedPairs addObject:mergedPair];
|
|
151
|
+
|
|
152
|
+
// 2. Start a brand new block.
|
|
153
|
+
currentPair = nextPair;
|
|
154
|
+
currentRange = nextRange;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Add the final block
|
|
159
|
+
StylePair *lastPair = [[StylePair alloc] init];
|
|
160
|
+
lastPair.rangeValue = [NSValue valueWithRange:currentRange];
|
|
161
|
+
lastPair.styleValue = currentPair.styleValue;
|
|
162
|
+
[mergedPairs addObject:lastPair];
|
|
163
|
+
|
|
164
|
+
return mergedPairs;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
- (void)drawBlockQuotes:(EnrichedTextInputView *)typedInput origin:(CGPoint)origin inputRange:(NSRange)inputRange
|
|
168
|
+
{
|
|
55
169
|
BlockQuoteStyle *bqStyle = typedInput->stylesDict[@([BlockQuoteStyle getStyleType])];
|
|
56
170
|
if(bqStyle == nullptr) { return; }
|
|
57
171
|
|
|
58
|
-
NSRange inputRange = NSMakeRange(0, typedInput->textView.textStorage.length);
|
|
59
|
-
|
|
60
172
|
// it isn't the most performant but we have to check for all the blockquotes each time and redraw them
|
|
61
173
|
NSArray *allBlockquotes = [bqStyle findAllOccurences:inputRange];
|
|
62
174
|
|
|
@@ -78,7 +190,10 @@ static void const *kInputKey = &kInputKey;
|
|
|
78
190
|
}
|
|
79
191
|
];
|
|
80
192
|
}
|
|
81
|
-
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
- (void)drawLists:(EnrichedTextInputView *)typedInput origin:(CGPoint)origin inputRange:(NSRange)inputRange
|
|
196
|
+
{
|
|
82
197
|
UnorderedListStyle *ulStyle = typedInput->stylesDict[@([UnorderedListStyle getStyleType])];
|
|
83
198
|
OrderedListStyle *olStyle = typedInput->stylesDict[@([OrderedListStyle getStyleType])];
|
|
84
199
|
if(ulStyle == nullptr || olStyle == nullptr) { return; }
|
|
@@ -40,4 +40,8 @@
|
|
|
40
40
|
withInput:(EnrichedTextInputView* _Nonnull)input
|
|
41
41
|
inRange:(NSRange)range
|
|
42
42
|
withCondition:(BOOL (NS_NOESCAPE ^_Nonnull)(id _Nullable value, NSRange range))condition;
|
|
43
|
+
+ (NSArray *_Nonnull)getRangesWithout
|
|
44
|
+
:(NSArray<NSNumber *> *_Nonnull)types
|
|
45
|
+
withInput:(EnrichedTextInputView* _Nonnull)input
|
|
46
|
+
inRange:(NSRange)range;
|
|
43
47
|
@end
|
|
@@ -152,4 +152,51 @@
|
|
|
152
152
|
return occurences;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
+ (NSArray *_Nonnull)getRangesWithout
|
|
156
|
+
:(NSArray<NSNumber *> *_Nonnull)types
|
|
157
|
+
withInput:(EnrichedTextInputView* _Nonnull)input
|
|
158
|
+
inRange:(NSRange)range
|
|
159
|
+
{
|
|
160
|
+
NSMutableArray<id> *activeStyleObjects = [[NSMutableArray alloc] init];
|
|
161
|
+
for(NSNumber *type in types) {
|
|
162
|
+
id<BaseStyleProtocol> styleClass = input->stylesDict[type];
|
|
163
|
+
[activeStyleObjects addObject:styleClass];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (activeStyleObjects.count == 0) {
|
|
167
|
+
return @[[NSValue valueWithRange:range]];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
NSMutableArray<NSValue *> *newRanges = [[NSMutableArray alloc] init];
|
|
171
|
+
NSUInteger lastRangeLocation = range.location;
|
|
172
|
+
NSUInteger endLocation = range.location + range.length;
|
|
173
|
+
|
|
174
|
+
for (NSUInteger i = range.location; i < endLocation; i++) {
|
|
175
|
+
NSRange currentRange = NSMakeRange(i, 1);
|
|
176
|
+
BOOL forbiddenStyleFound = NO;
|
|
177
|
+
|
|
178
|
+
for (id style in activeStyleObjects) {
|
|
179
|
+
if ([style detectStyle:currentRange]) {
|
|
180
|
+
forbiddenStyleFound = YES;
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (forbiddenStyleFound) {
|
|
186
|
+
if (i > lastRangeLocation) {
|
|
187
|
+
NSRange cleanRange = NSMakeRange(lastRangeLocation, i - lastRangeLocation);
|
|
188
|
+
[newRanges addObject:[NSValue valueWithRange:cleanRange]];
|
|
189
|
+
}
|
|
190
|
+
lastRangeLocation = i + 1;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (lastRangeLocation < endLocation) {
|
|
195
|
+
NSRange remainingRange = NSMakeRange(lastRangeLocation, endLocation - lastRangeLocation);
|
|
196
|
+
[newRanges addObject:[NSValue valueWithRange:remainingRange]];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return newRanges;
|
|
200
|
+
}
|
|
201
|
+
|
|
155
202
|
@end
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
|
|
9
9
|
// if the user backspaces the last character in a line, the iOS applies typing attributes from the previous line
|
|
10
10
|
// in the case of some paragraph styles it works especially bad when a list point just appears
|
|
11
|
-
//
|
|
11
|
+
// this method handles that case differently with or without present paragraph styles
|
|
12
12
|
+ (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text input:(id)input {
|
|
13
13
|
EnrichedTextInputView *typedInput = (EnrichedTextInputView *)input;
|
|
14
14
|
UnorderedListStyle *ulStyle = typedInput->stylesDict[@([UnorderedListStyle getStyleType])];
|
|
15
15
|
OrderedListStyle *olStyle = typedInput->stylesDict[@([OrderedListStyle getStyleType])];
|
|
16
16
|
BlockQuoteStyle *bqStyle = typedInput->stylesDict[@([BlockQuoteStyle getStyleType])];
|
|
17
|
+
CodeBlockStyle *cbStyle = typedInput->stylesDict[@([CodeBlockStyle getStyleType])];
|
|
17
18
|
|
|
18
19
|
if(typedInput == nullptr) {
|
|
19
20
|
return NO;
|
|
@@ -34,29 +35,25 @@
|
|
|
34
35
|
|
|
35
36
|
NSRange nonNewlineRange = [(NSValue *)paragraphs.firstObject rangeValue];
|
|
36
37
|
|
|
37
|
-
//
|
|
38
|
+
// the backspace removes the whole content of a paragraph (possibly more but has to start where the paragraph starts)
|
|
38
39
|
if(range.location == nonNewlineRange.location && range.length >= nonNewlineRange.length) {
|
|
39
|
-
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
[TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr input:typedInput withSelection:YES];
|
|
53
|
-
[bqStyle addAttributes:NSMakeRange(range.location, 0)];
|
|
54
|
-
return YES;
|
|
40
|
+
|
|
41
|
+
// for lists, quotes and codeblocks present we do the following:
|
|
42
|
+
// - manually do the removing
|
|
43
|
+
// - reset typing attribtues so that the previous line styles don't get applied
|
|
44
|
+
// - reapply the paragraph style that was present so that a zero width space appears here
|
|
45
|
+
NSArray *handledStyles = @[ulStyle, olStyle, bqStyle, cbStyle];
|
|
46
|
+
for(id<BaseStyleProtocol> style in handledStyles) {
|
|
47
|
+
if([style detectStyle:nonNewlineRange]) {
|
|
48
|
+
[TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr input:typedInput withSelection:YES];
|
|
49
|
+
typedInput->textView.typingAttributes = typedInput->defaultTypingAttributes;
|
|
50
|
+
[style addAttributes:NSMakeRange(range.location, 0)];
|
|
51
|
+
return YES;
|
|
52
|
+
}
|
|
55
53
|
}
|
|
56
54
|
|
|
57
|
-
// do the replacement manually
|
|
55
|
+
// otherwise (no paragraph styles present), we just do the replacement manually and reset typing attribtues
|
|
58
56
|
[TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr input:typedInput withSelection:YES];
|
|
59
|
-
// reset typing attribtues
|
|
60
57
|
typedInput->textView.typingAttributes = typedInput->defaultTypingAttributes;
|
|
61
58
|
return YES;
|
|
62
59
|
}
|
|
@@ -64,4 +61,74 @@
|
|
|
64
61
|
return NO;
|
|
65
62
|
}
|
|
66
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Handles the specific case of backspacing a newline character, which results in merging two paragraphs.
|
|
66
|
+
*
|
|
67
|
+
* THE PROBLEM:
|
|
68
|
+
* When merging a bottom paragraph (Source) into a top paragraph (Destination), the bottom paragraph
|
|
69
|
+
* normally brings all its attributes with it. If the top paragraph is a restrictive style (like a CodeBlock),
|
|
70
|
+
* and the bottom paragraph contains a conflicting style (like an H1 Header), a standard merge would
|
|
71
|
+
* create an invalid state (e.g., a CodeBlock that is also a Header).
|
|
72
|
+
*
|
|
73
|
+
* THE SOLUTION:
|
|
74
|
+
* 1. Identifies the dominant style of the paragraph ABOVE the deleted newline (`leftParagraphStyle`).
|
|
75
|
+
* 2. Checks the paragraph BELOW the newline (`rightRange`) for any styles that conflict with or are blocked by the top style.
|
|
76
|
+
* 3. Explicitly removes those forbidden styles from the bottom paragraph *before* the merge occurs.
|
|
77
|
+
* 4. Performs the merge (deletes the newline).
|
|
78
|
+
*
|
|
79
|
+
* @return YES if the newline backspace was handled and sanitized; NO otherwise.
|
|
80
|
+
*/
|
|
81
|
+
+ (BOOL)handleNewlineBackspaceInRange:(NSRange)range replacementText:(NSString *)text input:(id)input {
|
|
82
|
+
EnrichedTextInputView *typedInput = (EnrichedTextInputView *)input;
|
|
83
|
+
if(typedInput == nullptr) {
|
|
84
|
+
return NO;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if(text.length == 0 && range.length == 1 &&
|
|
88
|
+
[[NSCharacterSet newlineCharacterSet] characterIsMember:[typedInput->textView.textStorage.string characterAtIndex:range.location]]) {
|
|
89
|
+
NSRange leftRange = [typedInput->textView.textStorage.string paragraphRangeForRange:range];
|
|
90
|
+
|
|
91
|
+
id<BaseStyleProtocol> leftParagraphStyle = nullptr;
|
|
92
|
+
for (NSNumber *key in typedInput->stylesDict) {
|
|
93
|
+
id<BaseStyleProtocol> style = typedInput->stylesDict[key];
|
|
94
|
+
if([[style class] isParagraphStyle] && [style detectStyle:leftRange]) {
|
|
95
|
+
leftParagraphStyle = style;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if(leftParagraphStyle == nullptr) {
|
|
100
|
+
return NO;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// index out of bounds
|
|
104
|
+
if(range.location + 1 >= typedInput->textView.textStorage.string.length) {
|
|
105
|
+
return NO;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
NSRange rightRange = [typedInput->textView.textStorage.string paragraphRangeForRange:NSMakeRange(range.location + 1, 1)];
|
|
109
|
+
|
|
110
|
+
StyleType type = [[leftParagraphStyle class] getStyleType];
|
|
111
|
+
|
|
112
|
+
NSArray *conflictingStyles = [typedInput getPresentStyleTypesFrom:typedInput->conflictingStyles[@(type)] range:rightRange];
|
|
113
|
+
NSArray *blockingStyles = [typedInput getPresentStyleTypesFrom:typedInput->blockingStyles[@(type)] range:rightRange];
|
|
114
|
+
NSArray *allToBeRemoved = [conflictingStyles arrayByAddingObjectsFromArray:blockingStyles];
|
|
115
|
+
|
|
116
|
+
for(NSNumber *style in allToBeRemoved) {
|
|
117
|
+
id<BaseStyleProtocol> styleClass = typedInput->stylesDict[style];
|
|
118
|
+
|
|
119
|
+
// for ranges, we need to remove each occurence
|
|
120
|
+
NSArray<StylePair *> *allOccurences = [styleClass findAllOccurences:rightRange];
|
|
121
|
+
|
|
122
|
+
for(StylePair* pair in allOccurences) {
|
|
123
|
+
[styleClass removeAttributes: [pair.rangeValue rangeValue]];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
[TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr input:typedInput withSelection:YES];
|
|
128
|
+
return YES;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return NO;
|
|
132
|
+
}
|
|
133
|
+
|
|
67
134
|
@end
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
- (std::string)toCppString;
|
|
7
7
|
+ (NSString *)fromCppString:(std::string)string;
|
|
8
8
|
+ (NSString *)stringByEscapingHtml:(NSString *)html;
|
|
9
|
-
+ (
|
|
9
|
+
+ (NSDictionary *)getEscapedCharactersInfoFrom:(NSString *)text;
|
|
10
10
|
@end
|
|
11
11
|
|
|
12
12
|
@interface NSMutableString (StringExtension)
|