@@ -67,9 +67,9 @@ extension RichEditorState {
67
67
- style: is of type RichTextSpanStyle
68
68
*/
69
69
public func updateStyle( style: RichTextSpanStyle , shouldRegisterUndo: Bool = true ) {
70
+ setInternalStyles ( style: style)
70
71
setStyle ( style, shouldRegisterUndo: shouldRegisterUndo)
71
72
/// Don't change order of function call as it is comparing active attributes with new one so updating it before applying attribute will break the behavior of undo and redo
72
- setInternalStyles ( style: style)
73
73
}
74
74
}
75
75
@@ -150,8 +150,9 @@ extension RichEditorState {
150
150
if style. isHeaderStyle || style. isDefault //|| style.isList
151
151
|| style. isAlignmentStyle
152
152
{
153
+ let shouldAdd = style. isHeaderStyle ? style. headerType != . default : !style. isDefault
153
154
handleAddOrRemoveStyleToLine (
154
- in: selectedRange, style: style, byAdding: !style . isDefault )
155
+ in: selectedRange, style: style, byAdding: shouldAdd )
155
156
if shouldRegisterUndo {
156
157
registerUndoForSetStyle ( newStyle: style)
157
158
}
@@ -176,16 +177,17 @@ extension RichEditorState {
176
177
addStyle = CGFloat ( size) != CGFloat . standardRichTextFontSize
177
178
}
178
179
case . font( let fontName) :
179
- if let fontName {
180
- addStyle = fontName == self . fontName
180
+ let defaultName = RichTextFont . PickerFont. standardSystemFontDisplayName
181
+ if let fontName, fontName != defaultName {
182
+ addStyle = fontName != defaultName
183
+ } else {
184
+ addStyle = false
181
185
}
182
186
case . color( let color) :
183
187
let defaultColor = RichTextColor . foreground. adjust ( nil , for: colorScheme)
184
188
if let color, color. toHex ( ) != defaultColor. toHex ( ) {
185
189
if let internalColor = self . color ( for: . foreground) {
186
- addStyle = ( Color ( internalColor) != color)
187
- } else {
188
- addStyle = true
190
+ addStyle = ( Color ( internalColor) == color && Color ( internalColor) != defaultColor)
189
191
}
190
192
} else {
191
193
addStyle = false
@@ -194,9 +196,7 @@ extension RichEditorState {
194
196
let defaultColor = RichTextColor . background. adjust ( nil , for: colorScheme)
195
197
if let color = bgColor, color. toHex ( ) != defaultColor. toHex ( ) {
196
198
if let internalColor = self . color ( for: . background) {
197
- addStyle = ( Color ( internalColor) == color)
198
- } else {
199
- addStyle = true
199
+ addStyle = ( Color ( internalColor) == color && Color ( internalColor) != defaultColor)
200
200
}
201
201
} else {
202
202
addStyle = false
@@ -207,6 +207,7 @@ extension RichEditorState {
207
207
}
208
208
case . link( let link) :
209
209
addStyle = link != nil
210
+
210
211
default :
211
212
return addStyle
212
213
}
@@ -587,8 +588,10 @@ extension RichEditorState {
587
588
588
589
internalSpans. removeAll ( where: { selectedParts. contains ( $0) } )
589
590
}
591
+ }
590
592
591
- //MARK: - Add Header style
593
+ //MARK: - Process Spans for style
594
+ extension RichEditorState {
592
595
/**
593
596
This will create span for selected text with provided style
594
597
- Parameters:
@@ -604,151 +607,97 @@ extension RichEditorState {
604
607
605
608
var processedSpans : [ RichTextSpanInternal ] = [ ]
606
609
607
- let completeOverlap = getCompleteOverlappingSpans ( for: range)
608
- var partialOverlap = getPartialOverlappingSpans ( for: range)
609
- var sameSpans = getSameSpans ( for: range)
610
-
611
- partialOverlap. removeAll ( where: { completeOverlap. contains ( $0) } )
612
- sameSpans. removeAll ( where: { completeOverlap. contains ( $0) } )
613
-
614
- let partialOverlapSpan = processPartialOverlappingSpans (
615
- partialOverlap, range: range, style: style, addStyle: addStyle)
616
- let completeOverlapSpan = processCompleteOverlappingSpans (
617
- completeOverlap, range: range, style: style, addStyle: addStyle)
618
- let sameSpan = processSameSpans (
619
- sameSpans, range: range, style: style, addStyle: addStyle)
620
-
621
- processedSpans. append ( contentsOf: partialOverlapSpan)
622
- processedSpans. append ( contentsOf: completeOverlapSpan)
623
- processedSpans. append ( contentsOf: sameSpan)
624
-
625
- processedSpans = mergeSameStyledSpans ( processedSpans)
626
-
627
- internalSpans. removeAll ( where: {
628
- $0. closedRange. overlaps ( range. closedRange)
629
- } )
630
- internalSpans. append ( contentsOf: processedSpans)
631
- internalSpans = mergeSameStyledSpans ( internalSpans)
632
- internalSpans. sort ( by: { $0. from < $1. from } )
633
- }
634
-
635
- private func processCompleteOverlappingSpans(
636
- _ spans: [ RichTextSpanInternal ] , range: NSRange ,
637
- style: RichTextSpanStyle , addStyle: Bool = true
638
- ) -> [ RichTextSpanInternal ] {
639
- var processedSpans : [ RichTextSpanInternal ] = [ ]
640
-
641
- for span in spans {
642
- if span. closedRange. isInRange ( range. closedRange) {
643
- processedSpans. append (
644
- span. copy (
645
- attributes: span. attributes? . copy (
646
- with: style, byAdding: addStyle) ) )
647
- } else {
648
- if span. from < range. lowerBound {
649
- let leftPart = span. copy ( to: range. lowerBound - 1 )
650
- processedSpans. append ( leftPart)
651
- }
652
-
653
- if span. from <= ( range. lowerBound)
654
- && span. to >= ( range. upperBound - 1 )
655
- {
656
- let centerPart = span. copy (
657
- from: range. lowerBound, to: range. upperBound - 1 ,
658
- attributes: span. attributes? . copy (
659
- with: style, byAdding: addStyle) )
660
- processedSpans. append ( centerPart)
661
- }
662
-
663
- if span. to > ( range. upperBound - 1 ) {
664
- let rightPart = span. copy ( from: range. upperBound)
665
- processedSpans. append ( rightPart)
610
+ // First split existing spans at selection boundaries
611
+ let splitPoints = Set ( [ range. lowerBound, range. upperBound] )
612
+ var currentSpans = internalSpans
613
+
614
+ // Split spans at boundaries
615
+ for splitPoint in splitPoints {
616
+ currentSpans = currentSpans. flatMap { span -> [ RichTextSpanInternal ] in
617
+ if span. from < splitPoint && span. to >= splitPoint {
618
+ return [
619
+ span. copy ( to: splitPoint - 1 ) ,
620
+ span. copy ( from: splitPoint) ,
621
+ ]
666
622
}
623
+ return [ span]
667
624
}
668
625
}
669
626
670
- processedSpans = mergeSameStyledSpans ( processedSpans)
671
-
672
- return processedSpans
673
- }
674
-
675
- private func processPartialOverlappingSpans(
676
- _ spans: [ RichTextSpanInternal ] , range: NSRange ,
677
- style: RichTextSpanStyle , addStyle: Bool = true
678
- ) -> [ RichTextSpanInternal ] {
679
- var processedSpans : [ RichTextSpanInternal ] = [ ]
680
-
681
- for span in spans {
682
- if span. from < range. location {
683
- let leftPart = span. copy ( to: range. lowerBound - 1 )
684
- let rightPart = span. copy (
685
- from: range. lowerBound,
686
- attributes: span. attributes? . copy (
687
- with: style, byAdding: addStyle) )
688
- processedSpans. append ( leftPart)
689
- processedSpans. append ( rightPart)
627
+ // Process spans in selection range
628
+ for span in currentSpans {
629
+ if span. closedRange. overlaps ( range. closedRange) {
630
+ // Span is within selection - apply new style
631
+ if span. closedRange. isInRange ( range. closedRange) {
632
+ let newAttributes = span. attributes? . copy ( with: style, byAdding: addStyle)
633
+ processedSpans. append ( span. copy ( attributes: newAttributes) )
634
+ }
635
+ // Span partially overlaps - split and apply style only to overlapping part
636
+ else {
637
+ if span. from < range. lowerBound {
638
+ processedSpans. append ( span. copy ( to: range. lowerBound - 1 ) )
639
+ }
640
+
641
+ let overlapStart = max ( span. from, range. lowerBound)
642
+ let overlapEnd = min ( span. to, range. upperBound - 1 )
643
+
644
+ if overlapStart <= overlapEnd {
645
+ let newAttributes = span. attributes? . copy ( with: style, byAdding: addStyle)
646
+ processedSpans. append (
647
+ span. copy (
648
+ from: overlapStart,
649
+ to: overlapEnd,
650
+ attributes: newAttributes
651
+ )
652
+ )
653
+ }
654
+
655
+ if span. to >= range. upperBound {
656
+ processedSpans. append ( span. copy ( from: range. upperBound) )
657
+ }
658
+ }
690
659
} else {
691
- let leftPart = span. copy (
692
- to: min ( span. to, range. upperBound) ,
693
- attributes: span. attributes? . copy (
694
- with: style, byAdding: addStyle) )
695
- let rightPart = span. copy ( from: range. location)
696
- processedSpans. append ( leftPart)
697
- processedSpans. append ( rightPart)
660
+ // Span outside selection - keep unchanged
661
+ processedSpans. append ( span)
698
662
}
699
663
}
700
664
665
+ // Merge adjacent spans with identical styles
701
666
processedSpans = mergeSameStyledSpans ( processedSpans)
702
- return processedSpans
703
- }
704
667
705
- private func processSameSpans(
706
- _ spans: [ RichTextSpanInternal ] , range: NSRange ,
707
- style: RichTextSpanStyle , addStyle: Bool = true
708
- ) -> [ RichTextSpanInternal ] {
709
- var processedSpans : [ RichTextSpanInternal ] = [ ]
710
-
711
- processedSpans = spans. map ( {
712
- $0. copy (
713
- attributes: $0. attributes? . copy ( with: style, byAdding: addStyle)
714
- )
715
- } )
716
-
717
- processedSpans = mergeSameStyledSpans ( processedSpans)
718
- return processedSpans
668
+ // Update internal spans
669
+ internalSpans = processedSpans. sorted ( by: { $0. from < $1. from } )
719
670
}
720
671
721
- // merge adjacent spans with same style
722
- private func mergeSameStyledSpans( _ spans: [ RichTextSpanInternal ] )
723
- -> [ RichTextSpanInternal ]
724
- {
672
+ ///Merge adjacent spans with same style
673
+ private func mergeSameStyledSpans( _ spans: [ RichTextSpanInternal ] ) -> [ RichTextSpanInternal ] {
725
674
guard !spans. isEmpty else { return [ ] }
675
+
726
676
var mergedSpans : [ RichTextSpanInternal ] = [ ]
727
- var previousSpan : RichTextSpanInternal ?
677
+ var currentSpan : RichTextSpanInternal ? = spans [ 0 ]
728
678
729
- for span in spans. sorted ( by: { $0. from < $1. from } ) {
730
- if let current = previousSpan {
731
- if span. attributes? . stylesSet ( )
732
- == current. attributes? . stylesSet ( )
733
- {
734
- // Merge overlapping spans
735
- previousSpan = current. copy ( to: max ( current. to, span. to) )
736
- } else {
737
- // Add merged span and start a new span
738
- mergedSpans. append ( current)
739
- previousSpan = span
740
- }
679
+ for nextSpan in spans. dropFirst ( ) {
680
+ guard let current = currentSpan else {
681
+ currentSpan = nextSpan
682
+ continue
683
+ }
684
+
685
+ // Only merge if styles exactly match and spans are adjacent
686
+ if current. attributes? . stylesSet ( ) == nextSpan. attributes? . stylesSet ( )
687
+ && current. to + 1 == nextSpan. from
688
+ {
689
+ currentSpan = current. copy ( to: nextSpan. to)
741
690
} else {
742
- previousSpan = span
691
+ mergedSpans. append ( current)
692
+ currentSpan = nextSpan
743
693
}
744
694
}
745
695
746
- // Append the last current span
747
- if let lastSpan = previousSpan {
696
+ if let lastSpan = currentSpan {
748
697
mergedSpans. append ( lastSpan)
749
698
}
750
699
751
- return mergedSpans. sorted ( by : { $0 . from < $1 . from } )
700
+ return mergedSpans
752
701
}
753
702
}
754
703
@@ -783,61 +732,6 @@ extension RichEditorState {
783
732
}
784
733
}
785
734
786
- //MARK: - RichTextSpanInternal Helper
787
- extension RichEditorState {
788
- /**
789
- This will provide overlapping span for range
790
- - Parameters:
791
- - selectedRange: is of type NSRange
792
- */
793
- private func getOverlappingSpans( for selectedRange: NSRange )
794
- -> [ RichTextSpanInternal ]
795
- {
796
- return internalSpans. filter {
797
- $0. closedRange. overlaps ( selectedRange. closedRange)
798
- }
799
- }
800
-
801
- /**
802
- This will provide partial overlapping span for range
803
- - Parameters:
804
- - selectedRange: selectedRange is of type NSRange
805
- */
806
- func getPartialOverlappingSpans( for selectedRange: NSRange )
807
- -> [ RichTextSpanInternal ]
808
- {
809
- return getOverlappingSpans ( for: selectedRange) . filter ( {
810
- $0. closedRange. isPartialOverlap ( selectedRange. closedRange)
811
- } )
812
- }
813
-
814
- /**
815
- This will provide complete overlapping span for range
816
- - Parameters:
817
- - selectedRange: selectedRange is of type NSRange
818
- */
819
- func getCompleteOverlappingSpans( for selectedRange: NSRange )
820
- -> [ RichTextSpanInternal ]
821
- {
822
- return getOverlappingSpans ( for: selectedRange) . filter ( {
823
- $0. closedRange. isInRange ( selectedRange. closedRange)
824
- || selectedRange. closedRange. isInRange ( $0. closedRange)
825
- } )
826
- }
827
-
828
- /**
829
- This will provide same span for range
830
- - Parameters:
831
- - selectedRange: selectedRange is of type NSRange
832
- */
833
-
834
- func getSameSpans( for selectedRange: NSRange ) -> [ RichTextSpanInternal ] {
835
- return getOverlappingSpans ( for: selectedRange) . filter ( {
836
- $0. closedRange. isSameAs ( selectedRange. closedRange)
837
- } )
838
- }
839
- }
840
-
841
735
//MARK: - Helper Methods
842
736
extension RichEditorState {
843
737
/**
0 commit comments