-
Notifications
You must be signed in to change notification settings - Fork 67
Expand file tree
/
Copy pathOrderedListStyle.mm
More file actions
231 lines (201 loc) · 9.18 KB
/
Copy pathOrderedListStyle.mm
File metadata and controls
231 lines (201 loc) · 9.18 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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
#import "StyleHeaders.h"
#import "EnrichedTextInputView.h"
#import "FontExtension.h"
#import "OccurenceUtils.h"
#import "ParagraphsUtils.h"
#import "TextInsertionUtils.h"
@implementation OrderedListStyle {
EnrichedTextInputView *_input;
}
+ (StyleType)getStyleType { return OrderedList; }
- (CGFloat)getHeadIndent {
// lists are drawn manually
// margin before marker + gap between marker and paragraph
return [_input->config orderedListMarginLeft] + [_input->config orderedListGapWidth];
}
- (instancetype)initWithInput:(id)input {
self = [super init];
_input = (EnrichedTextInputView *)input;
return self;
}
- (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];
}
}
// we assume correct paragraph range is already given
- (void)addAttributes:(NSRange)range {
NSTextList *numberBullet = [[NSTextList alloc] initWithMarkerFormat:NSTextListMarkerDecimal options:0];
NSArray *paragraphs = [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView range:range];
// if we fill empty lines with zero width spaces, we need to offset later ranges
NSInteger offset = 0;
// needed for range adjustments
NSRange preModificationRange = _input->textView.selectedRange;
// let's not emit some weird selection changes or text/html changes
_input->blockEmitting = YES;
for(NSValue *value in paragraphs) {
// take previous offsets into consideration
NSRange fixedRange = NSMakeRange([value rangeValue].location + offset, [value rangeValue].length);
// length 0 with first line, length 1 and newline with some empty lines in the middle
if(fixedRange.length == 0 ||
(fixedRange.length == 1 &&
[[NSCharacterSet newlineCharacterSet] characterIsMember: [_input->textView.textStorage.string characterAtIndex:fixedRange.location]])
) {
[TextInsertionUtils insertText:@"\u200B" at:fixedRange.location additionalAttributes:nullptr input:_input withSelection:NO];
fixedRange = NSMakeRange(fixedRange.location, fixedRange.length + 1);
offset += 1;
}
[_input->textView.textStorage enumerateAttribute:NSParagraphStyleAttributeName inRange:fixedRange options:0
usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) {
NSMutableParagraphStyle *pStyle = [(NSParagraphStyle *)value mutableCopy];
pStyle.textLists = @[numberBullet];
pStyle.headIndent = [self getHeadIndent];
pStyle.firstLineHeadIndent = [self getHeadIndent];
[_input->textView.textStorage addAttribute:NSParagraphStyleAttributeName value:pStyle range:range];
}
];
}
// back to emitting
_input->blockEmitting = NO;
if(preModificationRange.length == 0) {
// fix selection if only one line was possibly made a list and filled with a space
_input->textView.selectedRange = preModificationRange;
} else {
// in other cases, fix the selection with newly made offsets
_input->textView.selectedRange = NSMakeRange(preModificationRange.location, preModificationRange.length + offset);
}
// also add typing attributes
NSMutableDictionary *typingAttrs = [_input->textView.typingAttributes mutableCopy];
NSMutableParagraphStyle *pStyle = [typingAttrs[NSParagraphStyleAttributeName] mutableCopy];
pStyle.textLists = @[numberBullet];
pStyle.headIndent = [self getHeadIndent];
pStyle.firstLineHeadIndent = [self getHeadIndent];
typingAttrs[NSParagraphStyleAttributeName] = pStyle;
_input->textView.typingAttributes = typingAttrs;
}
// does pretty much the same as normal addAttributes, just need to get the range
- (void)addTypingAttributes {
[self addAttributes:_input->textView.selectedRange];
}
- (void)removeAttributes:(NSRange)range {
NSArray *paragraphs = [ParagraphsUtils getSeparateParagraphsRangesIn:_input->textView range:range];
[_input->textView.textStorage beginEditing];
for(NSValue *value in paragraphs) {
NSRange range = [value rangeValue];
[_input->textView.textStorage enumerateAttribute:NSParagraphStyleAttributeName inRange:range options:0
usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) {
NSMutableParagraphStyle *pStyle = [(NSParagraphStyle *)value mutableCopy];
pStyle.textLists = @[];
pStyle.headIndent = 0;
pStyle.firstLineHeadIndent = 0;
[_input->textView.textStorage addAttribute:NSParagraphStyleAttributeName value:pStyle range:range];
}
];
}
[_input->textView.textStorage endEditing];
// also remove typing attributes
NSMutableDictionary *typingAttrs = [_input->textView.typingAttributes mutableCopy];
NSMutableParagraphStyle *pStyle = [typingAttrs[NSParagraphStyleAttributeName] mutableCopy];
pStyle.textLists = @[];
pStyle.headIndent = 0;
pStyle.firstLineHeadIndent = 0;
typingAttrs[NSParagraphStyleAttributeName] = pStyle;
_input->textView.typingAttributes = typingAttrs;
}
// needed for the sake of style conflicts, needs to do exactly the same as removeAttribtues
- (void)removeTypingAttributes {
[self removeAttributes:_input->textView.selectedRange];
}
- (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text {
if(
[self detectStyle:_input->textView.selectedRange] &&
NSEqualRanges(_input->textView.selectedRange, NSMakeRange(0, 0)) &&
[text isEqualToString:@""]
) {
// removing first list point by backspacing doesn't remove typing attributes because it doesn't run textViewDidChange
// so we try guessing that a point should be deleted here
NSRange paragraphRange = [_input->textView.textStorage.string paragraphRangeForRange:_input->textView.selectedRange];
[self removeAttributes:paragraphRange];
return YES;
} else if(
[self detectStyle:_input->textView.selectedRange] &&
[text isEqualToString:@""]
) {
// other case; make sure removing all the (non newline) text from a list item also removes the item itself
NSRange paragraphRange = [_input->textView.textStorage.string paragraphRangeForRange:range];
NSValue *nonNewlineVal = [ParagraphsUtils getNonNewlineRangesIn:_input->textView range:paragraphRange].firstObject;
if(nonNewlineVal == nullptr) {
return NO;
}
NSRange nonNewlineRange = [nonNewlineVal rangeValue];
if(NSEqualRanges(range, nonNewlineRange)) {
[self removeAttributes:range];
[TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr input:_input withSelection:YES];
return YES;
}
}
return NO;
}
- (BOOL)tryHandlingListShorcutInRange:(NSRange)range replacementText:(NSString *)text {
NSRange paragraphRange = [_input->textView.textStorage.string paragraphRangeForRange:range];
// a dot was added - check if we are both at the paragraph beginning + 1 character (which we want to be a dash)
if([text isEqualToString:@"."] && range.location - 1 == paragraphRange.location) {
unichar charBefore = [_input->textView.textStorage.string characterAtIndex:range.location - 1];
if(charBefore == '1') {
// we got a match - add a list if possible
if([_input handleStyleBlocksAndConflicts:[[self class] getStyleType] range:paragraphRange]) {
// don't emit some html updates during the replacing
BOOL prevEmitHtml = _input->emitHtml;
if(prevEmitHtml) {
_input->emitHtml = NO;
}
// remove the number
[TextInsertionUtils replaceText:@"" at:NSMakeRange(paragraphRange.location, 1) additionalAttributes:nullptr input:_input withSelection:YES];
if(prevEmitHtml) {
_input->emitHtml = YES;
}
// add attributes on the paragraph
[self addAttributes:NSMakeRange(paragraphRange.location, paragraphRange.length - 1)];
return YES;
}
}
}
return NO;
}
- (BOOL)styleCondition:(id _Nullable)value :(NSRange)range {
NSParagraphStyle *paragraph = (NSParagraphStyle *)value;
return paragraph != nullptr && paragraph.textLists.count == 1 && paragraph.textLists.firstObject.markerFormat == NSTextListMarkerDecimal;
}
- (BOOL)detectStyle:(NSRange)range {
if(range.length >= 1) {
return [OccurenceUtils detect:NSParagraphStyleAttributeName withInput:_input inRange:range
withCondition: ^BOOL(id _Nullable value, NSRange range) {
return [self styleCondition:value :range];
}
];
} else {
return [OccurenceUtils detect:NSParagraphStyleAttributeName withInput:_input atIndex:range.location checkPrevious:YES
withCondition:^BOOL(id _Nullable value, NSRange range) {
return [self styleCondition:value :range];
}
];
}
}
- (BOOL)anyOccurence:(NSRange)range {
return [OccurenceUtils any:NSParagraphStyleAttributeName withInput:_input inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
return [self styleCondition:value :range];
}
];
}
- (NSArray<StylePair *> *_Nullable)findAllOccurences:(NSRange)range {
return [OccurenceUtils all:NSParagraphStyleAttributeName withInput:_input inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
return [self styleCondition:value :range];
}
];
}
@end