-
Notifications
You must be signed in to change notification settings - Fork 67
Expand file tree
/
Copy pathHeadingStyleBase.mm
More file actions
176 lines (156 loc) · 6.7 KB
/
Copy pathHeadingStyleBase.mm
File metadata and controls
176 lines (156 loc) · 6.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
#import "StyleHeaders.h"
#import "EnrichedTextInputView.h"
#import "FontExtension.h"
#import "OccurenceUtils.h"
#import "TextInsertionUtils.h"
@implementation HeadingStyleBase
// mock values since H1/2/3Style classes anyway are used
+ (StyleType)getStyleType { return None; }
- (CGFloat)getHeadingFontSize { return 0; }
- (BOOL)isHeadingBold { return false; }
- (EnrichedTextInputView *)typedInput {
return (EnrichedTextInputView *)input;
}
- (instancetype)initWithInput:(id)input {
self = [super init];
self->input = input;
return self;
}
// the range will already be the full paragraph/s range
// but if the paragraph is empty it still is of length 0
- (void)applyStyle:(NSRange)range {
BOOL isStylePresent = [self detectStyle:range];
if(range.length >= 1) {
isStylePresent ? [self removeAttributes:range] : [self addAttributes:range];
} else {
isStylePresent ? [self removeTypingAttributes] : [self addTypingAttributes];
}
}
// the range will already be the proper full paragraph/s range
- (void)addAttributes:(NSRange)range {
[[self typedInput]->textView.textStorage beginEditing];
[[self typedInput]->textView.textStorage enumerateAttribute:NSFontAttributeName inRange:range options:0
usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) {
UIFont *font = (UIFont *)value;
if(font != nullptr) {
UIFont *newFont = [font setSize:[self getHeadingFontSize]];
if([self isHeadingBold]) {
newFont = [newFont setBold];
}
[[self typedInput]->textView.textStorage addAttribute:NSFontAttributeName value:newFont range:range];
}
}
];
[[self typedInput]->textView.textStorage endEditing];
// also toggle typing attributes
[self addTypingAttributes];
}
// will always be called on empty paragraphs so only typing attributes can be changed
- (void)addTypingAttributes {
UIFont *currentFontAttr = (UIFont *)[self typedInput]->textView.typingAttributes[NSFontAttributeName];
if(currentFontAttr != nullptr) {
NSMutableDictionary *newTypingAttrs = [[self typedInput]->textView.typingAttributes mutableCopy];
UIFont *newFont = [currentFontAttr setSize:[self getHeadingFontSize]];
if([self isHeadingBold]) {
newFont = [newFont setBold];
}
newTypingAttrs[NSFontAttributeName] = newFont;
[self typedInput]->textView.typingAttributes = newTypingAttrs;
}
}
// we need to remove the style from the whole paragraph
- (void)removeAttributes:(NSRange)range {
NSRange paragraphRange = [[self typedInput]->textView.textStorage.string paragraphRangeForRange:range];
[[self typedInput]->textView.textStorage beginEditing];
[[self typedInput]->textView.textStorage enumerateAttribute:NSFontAttributeName inRange:paragraphRange options:0
usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) {
if([self styleCondition:value :range]) {
UIFont *newFont = [(UIFont *)value setSize:[[[self typedInput]->config primaryFontSize] floatValue]];
if([self isHeadingBold]) {
newFont = [newFont removeBold];
}
[[self typedInput]->textView.textStorage addAttribute:NSFontAttributeName value:newFont range:range];
}
}
];
[[self typedInput]->textView.textStorage endEditing];
// typing attributes still need to be removed
UIFont *currentFontAttr = (UIFont *)[self typedInput]->textView.typingAttributes[NSFontAttributeName];
if(currentFontAttr != nullptr) {
NSMutableDictionary *newTypingAttrs = [[self typedInput]->textView.typingAttributes mutableCopy];
UIFont *newFont = [currentFontAttr setSize:[[[self typedInput]->config primaryFontSize] floatValue]];
if([self isHeadingBold]) {
newFont = [newFont removeBold];
}
newTypingAttrs[NSFontAttributeName] = newFont;
[self typedInput]->textView.typingAttributes = newTypingAttrs;
}
}
- (void)removeTypingAttributes {
// all the heading still needs to be removed because this function may be called in conflicting styles logic
// typing attributes already get removed in there as well
[self removeAttributes:[self typedInput]->textView.selectedRange];
}
- (BOOL)styleCondition:(id _Nullable)value :(NSRange)range {
UIFont *font = (UIFont *)value;
return font != nullptr && font.pointSize == [self getHeadingFontSize];
}
- (BOOL)detectStyle:(NSRange)range {
if(range.length >= 1) {
return [OccurenceUtils detect:NSFontAttributeName withInput:[self typedInput] inRange:range
withCondition: ^BOOL(id _Nullable value, NSRange range) {
return [self styleCondition:value :range];
}
];
} else {
return [OccurenceUtils detect:NSFontAttributeName withInput:[self typedInput] atIndex:range.location checkPrevious:YES
withCondition:^BOOL(id _Nullable value, NSRange range) {
return [self styleCondition:value :range];
}
];
}
}
- (BOOL)anyOccurence:(NSRange)range {
return [OccurenceUtils any:NSFontAttributeName withInput:[self typedInput] inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
return [self styleCondition:value :range];
}
];
}
- (NSArray<StylePair *> *_Nullable)findAllOccurences:(NSRange)range {
return [OccurenceUtils all:NSFontAttributeName withInput:[self typedInput] inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
return [self styleCondition:value :range];
}
];
}
// used to make sure headings dont persist after a newline is placed
- (BOOL)handleNewlinesInRange:(NSRange)range replacementText:(NSString *)text {
// in a heading and a new text ends with a newline
if(
[self detectStyle:[self typedInput]->textView.selectedRange] &&
text.length > 0 &&
[[NSCharacterSet newlineCharacterSet] characterIsMember: [text characterAtIndex:text.length-1]]
) {
// do the replacement manually
[TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr input:[self typedInput] withSelection:YES];
// remove the attribtues at the new selection
[self removeAttributes:[self typedInput]->textView.selectedRange];
return YES;
}
return NO;
}
// backspacing a line after a heading "into" a heading will not result in the text attaining heading attributes
// so, we do it manually
- (void)handleImproperHeadings {
NSArray *occurences = [self findAllOccurences:NSMakeRange(0, [self typedInput]->textView.textStorage.string.length)];
for(StylePair *pair in occurences) {
NSRange occurenceRange = [pair.rangeValue rangeValue];
NSRange paragraphRange = [[self typedInput]->textView.textStorage.string paragraphRangeForRange:occurenceRange];
if(!NSEqualRanges(occurenceRange, paragraphRange)) {
// we have a heading but it does not span its whole paragraph - let's fix it
[self addAttributes:paragraphRange];
}
}
}
@end