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.
Files changed (80) hide show
  1. package/README.md +3 -9
  2. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerDelegate.java +4 -1
  3. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerInterface.java +2 -1
  4. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.cpp +5 -0
  5. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.h +1 -45
  6. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt +53 -12
  7. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewLayoutManager.kt +7 -56
  8. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt +19 -22
  9. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewPackage.kt +2 -0
  10. package/android/src/main/java/com/swmansion/enriched/MeasurementStore.kt +158 -0
  11. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedCodeBlockSpan.kt +36 -1
  12. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedImageSpan.kt +132 -11
  13. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt +65 -46
  14. package/android/src/main/java/com/swmansion/enriched/spans/utils/ForceRedrawSpan.kt +13 -0
  15. package/android/src/main/java/com/swmansion/enriched/styles/HtmlStyle.kt +2 -9
  16. package/android/src/main/java/com/swmansion/enriched/styles/InlineStyles.kt +1 -0
  17. package/android/src/main/java/com/swmansion/enriched/styles/ParagraphStyles.kt +110 -3
  18. package/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt +75 -32
  19. package/android/src/main/java/com/swmansion/enriched/utils/AsyncDrawable.kt +91 -0
  20. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedParser.java +38 -15
  21. package/android/src/main/java/com/swmansion/enriched/utils/ResourceManager.kt +26 -0
  22. package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedSpanWatcher.kt +3 -1
  23. package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedTextWatcher.kt +1 -1
  24. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.cpp +15 -2
  25. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.h +1 -0
  26. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputShadowNode.cpp +1 -2
  27. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/conversions.h +27 -0
  28. package/android/src/main/res/drawable/broken_image.xml +10 -0
  29. package/ios/EnrichedTextInputView.h +3 -1
  30. package/ios/EnrichedTextInputView.mm +167 -68
  31. package/ios/config/InputConfig.h +6 -0
  32. package/ios/config/InputConfig.mm +32 -0
  33. package/ios/generated/RNEnrichedTextInputViewSpec/Props.cpp +5 -0
  34. package/ios/generated/RNEnrichedTextInputViewSpec/Props.h +1 -45
  35. package/ios/generated/RNEnrichedTextInputViewSpec/RCTComponentViewHelpers.h +20 -4
  36. package/ios/inputParser/InputParser.mm +179 -31
  37. package/ios/inputTextView/InputTextView.mm +3 -5
  38. package/ios/internals/EnrichedTextInputViewShadowNode.h +1 -0
  39. package/ios/internals/EnrichedTextInputViewShadowNode.mm +29 -17
  40. package/ios/styles/BlockQuoteStyle.mm +5 -26
  41. package/ios/styles/BoldStyle.mm +2 -0
  42. package/ios/styles/CodeBlockStyle.mm +228 -0
  43. package/ios/styles/H1Style.mm +1 -0
  44. package/ios/styles/H2Style.mm +1 -0
  45. package/ios/styles/H3Style.mm +1 -0
  46. package/ios/styles/ImageStyle.mm +158 -0
  47. package/ios/styles/InlineCodeStyle.mm +2 -0
  48. package/ios/styles/ItalicStyle.mm +2 -0
  49. package/ios/styles/LinkStyle.mm +15 -7
  50. package/ios/styles/MentionStyle.mm +133 -36
  51. package/ios/styles/OrderedListStyle.mm +5 -8
  52. package/ios/styles/StrikethroughStyle.mm +2 -0
  53. package/ios/styles/UnderlineStyle.mm +2 -0
  54. package/ios/styles/UnorderedListStyle.mm +5 -8
  55. package/ios/utils/BaseStyleProtocol.h +1 -0
  56. package/ios/utils/ImageData.h +10 -0
  57. package/ios/utils/ImageData.mm +4 -0
  58. package/ios/utils/LayoutManagerExtension.mm +118 -3
  59. package/ios/utils/OccurenceUtils.h +4 -0
  60. package/ios/utils/OccurenceUtils.mm +47 -0
  61. package/ios/utils/ParagraphAttributesUtils.h +1 -0
  62. package/ios/utils/ParagraphAttributesUtils.mm +87 -20
  63. package/ios/utils/StringExtension.h +1 -1
  64. package/ios/utils/StringExtension.mm +17 -8
  65. package/ios/utils/StyleHeaders.h +12 -0
  66. package/ios/utils/ZeroWidthSpaceUtils.mm +22 -10
  67. package/lib/module/EnrichedTextInput.js +4 -2
  68. package/lib/module/EnrichedTextInput.js.map +1 -1
  69. package/lib/module/EnrichedTextInputNativeComponent.ts +7 -5
  70. package/lib/module/normalizeHtmlStyle.js +0 -4
  71. package/lib/module/normalizeHtmlStyle.js.map +1 -1
  72. package/lib/typescript/src/EnrichedTextInput.d.ts +3 -6
  73. package/lib/typescript/src/EnrichedTextInput.d.ts.map +1 -1
  74. package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts +2 -5
  75. package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts.map +1 -1
  76. package/lib/typescript/src/normalizeHtmlStyle.d.ts.map +1 -1
  77. package/package.json +1 -1
  78. package/src/EnrichedTextInput.tsx +6 -7
  79. package/src/EnrichedTextInputNativeComponent.ts +7 -5
  80. 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 current word if it exists
301
- // we can be using current word only thanks to the fact that ongoing mentions are always one word (in contrast to ready, added mentions)
302
- NSDictionary *currentWord = [WordsUtils getCurrentWord:_input->textView.textStorage.string range:_input->textView.selectedRange];
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
- NSRange wordRange = [wordRangeValue rangeValue];
316
-
317
- // check for mentionIndicators - no sign of them means we shouldn't be editing a mention
318
- unichar firstChar = [wordText characterAtIndex:0];
319
- if(![[_input->config mentionIndicators] containsObject: @(firstChar)]) {
320
- [self removeActiveMentionRange];
321
- return;
322
- }
323
-
324
- // check for existing mentions - we don't edit them
325
- if([self detectStyle:wordRange]) {
326
- [self removeActiveMentionRange];
327
- return;
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 there is any sign of conflicting style classes, stop editing a mention
339
- if([linkStyle anyOccurence:wordRange] || [inlineCodeStyle anyOccurence:wordRange]) {
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:wordRange text:wordText];
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 some html updates during the replacing
162
- BOOL prevEmitHtml = _input->emitHtml;
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
- if(prevEmitHtml) {
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)];
@@ -8,6 +8,8 @@
8
8
 
9
9
  + (StyleType)getStyleType { return Strikethrough; }
10
10
 
11
+ + (BOOL)isParagraphStyle { return NO; }
12
+
11
13
  - (instancetype)initWithInput:(id)input {
12
14
  self = [super init];
13
15
  _input = (EnrichedTextInputView *)input;
@@ -8,6 +8,8 @@
8
8
 
9
9
  + (StyleType)getStyleType { return Underline; }
10
10
 
11
+ + (BOOL)isParagraphStyle { return NO; }
12
+
11
13
  - (instancetype)initWithInput:(id)input {
12
14
  self = [super init];
13
15
  _input = (EnrichedTextInputView *)input;
@@ -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 some html updates during the replacing
162
- BOOL prevEmitHtml = _input->emitHtml;
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
- if(prevEmitHtml) {
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)];
@@ -4,6 +4,7 @@
4
4
 
5
5
  @protocol BaseStyleProtocol <NSObject>
6
6
  + (StyleType)getStyleType;
7
+ + (BOOL)isParagraphStyle;
7
8
  - (instancetype _Nonnull)initWithInput:(id _Nonnull)input;
8
9
  - (void)applyStyle:(NSRange)range;
9
10
  - (void)addAttributes:(NSRange)range;
@@ -0,0 +1,10 @@
1
+ #pragma once
2
+ #import <UIKit/UIKit.h>
3
+
4
+ @interface ImageData : NSObject
5
+
6
+ @property NSString *uri;
7
+ @property CGFloat width;
8
+ @property CGFloat height;
9
+
10
+ @end
@@ -0,0 +1,4 @@
1
+ #import "ImageData.h"
2
+
3
+ @implementation ImageData
4
+ @end
@@ -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
@@ -3,4 +3,5 @@
3
3
 
4
4
  @interface ParagraphAttributesUtils : NSObject
5
5
  + (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text input:(id)input;
6
+ + (BOOL)handleNewlineBackspaceInRange:(NSRange)range replacementText:(NSString *)text input:(id)input;
6
7
  @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
- // hence the solution - reset typing attributes
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
- // if the backspace removes the whole content of a paragraph (possibly more but has to start where the paragraph starts), we remove the typing attributes
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
- // for lists and quotes we want to remove the characters but keep attribtues so that a zero width space appears here
40
- // so we do the removing manually and reapply attributes
41
- if([ulStyle detectStyle:nonNewlineRange]) {
42
- [TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr input:typedInput withSelection:YES];
43
- [ulStyle addAttributes:NSMakeRange(range.location, 0)];
44
- return YES;
45
- }
46
- if([olStyle detectStyle:nonNewlineRange]) {
47
- [TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr input:typedInput withSelection:YES];
48
- [olStyle addAttributes:NSMakeRange(range.location, 0)];
49
- return YES;
50
- }
51
- if([bqStyle detectStyle:nonNewlineRange]) {
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
- + (NSString *)stringByUnescapingHtml:(NSString *)html;
9
+ + (NSDictionary *)getEscapedCharactersInfoFrom:(NSString *)text;
10
10
  @end
11
11
 
12
12
  @interface NSMutableString (StringExtension)