-
Notifications
You must be signed in to change notification settings - Fork 53
Expand file tree
/
Copy pathEditorParser.mm
More file actions
669 lines (592 loc) · 30.5 KB
/
Copy pathEditorParser.mm
File metadata and controls
669 lines (592 loc) · 30.5 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
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
#import "EditorParser.h"
#import "ReactNativeRichTextEditorView.h"
#import "StyleHeaders.h"
#import "UIView+React.h"
#import "TextInsertionUtils.h"
@implementation EditorParser {
ReactNativeRichTextEditorView *_editor;
}
- (instancetype)initWithEditor:(id)editor {
self = [super init];
_editor = (ReactNativeRichTextEditorView *) editor;
return self;
}
- (NSString *)parseToHtmlFromRange:(NSRange)range {
NSInteger offset = range.location;
NSString *text = [_editor->textView.textStorage.string substringWithRange:range];
if(text.length == 0) {
return @"<html>\n<p></p>\n</html>";
}
NSMutableString *result = [[NSMutableString alloc] initWithString: @"<html>"];
NSSet<NSNumber *>*previousActiveStyles = [[NSSet<NSNumber *> alloc]init];
BOOL newLine = YES;
BOOL inUnorderedList = NO;
BOOL inOrderedList = NO;
BOOL inBlockQuote = NO;
unichar lastCharacter = 0;
for(int i = 0; i < text.length; i++) {
NSRange currentRange = NSMakeRange(offset + i, 1);
NSMutableSet<NSNumber *>*currentActiveStyles = [[NSMutableSet<NSNumber *> alloc]init];
NSMutableDictionary *currentActiveStylesBeginning = [[NSMutableDictionary alloc] init];
// check each existing style existence
for(NSNumber* type in _editor->stylesDict) {
id<BaseStyleProtocol> style = _editor->stylesDict[type];
if([style detectStyle:currentRange]) {
[currentActiveStyles addObject:type];
if(![previousActiveStyles member:type]) {
currentActiveStylesBeginning[type] = [NSNumber numberWithInt:i];
}
} else if([previousActiveStyles member:type]) {
[currentActiveStylesBeginning removeObjectForKey:type];
}
}
NSString *currentCharacterStr = [_editor->textView.textStorage.string substringWithRange:currentRange];
unichar currentCharacterChar = [_editor->textView.textStorage.string characterAtIndex:currentRange.location];
if([[NSCharacterSet newlineCharacterSet] characterIsMember:currentCharacterChar]) {
if(newLine) {
// we can either have an empty list item OR need to close the list and put a BR in such a situation
// the existence of the list must be checked on 0 length range, not on the newline character
if(inOrderedList) {
OrderedListStyle *oStyle = _editor->stylesDict[@(OrderedList)];
BOOL detected = [oStyle detectStyle: NSMakeRange(currentRange.location, 0)];
if(detected) {
[result appendString:@"\n<li></li>"];
} else {
[result appendString:@"\n</ol>\n<br>"];
inOrderedList = NO;
}
} else if(inUnorderedList) {
UnorderedListStyle *uStyle = _editor->stylesDict[@(UnorderedList)];
BOOL detected = [uStyle detectStyle: NSMakeRange(currentRange.location, 0)];
if(detected) {
[result appendString:@"\n<li></li>"];
} else {
[result appendString:@"\n</ul>\n<br>"];
inUnorderedList = NO;
}
} else {
[result appendString:@"\n<br>"];
}
} else {
// newline finishes a paragraph and all style tags need to be closed
// we use previous styles
NSArray<NSNumber*> *sortedEndedStyles = [previousActiveStyles sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"intValue" ascending:NO]]];
// append closing tags
for(NSNumber *style in sortedEndedStyles) {
NSString *tagContent = [self tagContentForStyle:style openingTag:NO location:currentRange.location];
[result appendString: [NSString stringWithFormat:@"</%@>", tagContent]];
}
// append closing paragraph tag
if([previousActiveStyles containsObject:@([UnorderedListStyle getStyleType])] ||
[previousActiveStyles containsObject:@([OrderedListStyle getStyleType])] ||
[previousActiveStyles containsObject:@([H1Style getStyleType])] ||
[previousActiveStyles containsObject:@([H2Style getStyleType])] ||
[previousActiveStyles containsObject:@([H3Style getStyleType])] ||
[previousActiveStyles containsObject:@([BlockQuoteStyle getStyleType])]
) {
// do nothing, proper closing paragraph tags have been already appended
} else {
[result appendString:@"</p>"];
}
}
// clear the previous styles
previousActiveStyles = [[NSSet<NSNumber *> alloc]init];
// next character opens new paragraph
newLine = YES;
} else {
// new line - open the paragraph
if(newLine) {
newLine = NO;
// handle ending unordered list
if(inUnorderedList && ![currentActiveStyles containsObject:@([UnorderedListStyle getStyleType])]) {
inUnorderedList = NO;
[result appendString:@"\n</ul>"];
}
// handle ending ordered list
if(inOrderedList && ![currentActiveStyles containsObject:@([OrderedListStyle getStyleType])]) {
inOrderedList = NO;
[result appendString:@"\n</ol>"];
}
// handle ending blockquotes
if(inBlockQuote && ![currentActiveStyles containsObject:@([BlockQuoteStyle getStyleType])]) {
inBlockQuote = NO;
[result appendString:@"\n</blockquote>"];
}
// handle starting unordered list
if(!inUnorderedList && [currentActiveStyles containsObject:@([UnorderedListStyle getStyleType])]) {
inUnorderedList = YES;
[result appendString:@"\n<ul>"];
}
// handle starting ordered list
if(!inOrderedList && [currentActiveStyles containsObject:@([OrderedListStyle getStyleType])]) {
inOrderedList = YES;
[result appendString:@"\n<ol>"];
}
// handle starting blockquotes
if(!inBlockQuote && [currentActiveStyles containsObject:@([BlockQuoteStyle getStyleType])]) {
inBlockQuote = YES;
[result appendString:@"\n<blockquote>"];
}
// don't add the <p> tag if some paragraph styles are present
if([currentActiveStyles containsObject:@([UnorderedListStyle getStyleType])] ||
[currentActiveStyles containsObject:@([OrderedListStyle getStyleType])] ||
[currentActiveStyles containsObject:@([H1Style getStyleType])] ||
[currentActiveStyles containsObject:@([H2Style getStyleType])] ||
[currentActiveStyles containsObject:@([H3Style getStyleType])] ||
[currentActiveStyles containsObject:@([BlockQuoteStyle getStyleType])]
) {
[result appendString:@"\n"];
} else {
[result appendString:@"\n<p>"];
}
}
// get styles that have ended
NSMutableSet<NSNumber *> *endedStyles = [previousActiveStyles mutableCopy];
[endedStyles minusSet: currentActiveStyles];
// also finish styles that should be ended becasue they are nested in a style that ended
NSMutableSet *fixedEndedStyles = [endedStyles mutableCopy];
NSMutableSet *stylesToBeReAdded = [[NSMutableSet alloc] init];
for(NSNumber *style in endedStyles) {
NSInteger styleBeginning = [currentActiveStylesBeginning[style] integerValue];
for(NSNumber *activeStyle in currentActiveStyles) {
NSInteger activeStyleBeginning = [currentActiveStylesBeginning[activeStyle] integerValue];
// we end the styles that began after the currently ended style
// also the ones that ended in the exact same place but are "inner" in relation to them due to StyleTypeEnum integer values
// "activeStylesBeginning < i" is needed, so that we don't remove styles that have been freshly added now
if((activeStyleBeginning > styleBeginning) ||
(activeStyleBeginning == styleBeginning && activeStyleBeginning < i && [activeStyle integerValue] > [style integerValue])) {
[fixedEndedStyles addObject:activeStyle];
[stylesToBeReAdded addObject:activeStyle];
}
}
}
// they are sorted in a descending order
NSArray<NSNumber*> *sortedEndedStyles = [fixedEndedStyles sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"intValue" ascending:NO]]];
// append closing tags
for(NSNumber *style in sortedEndedStyles) {
NSString *tagContent = [self tagContentForStyle:style openingTag:NO location:currentRange.location];
[result appendString: [NSString stringWithFormat:@"</%@>", tagContent]];
}
// get styles that have begun: they are sorted in a ascending manner to properly keep tags' FILO order
NSMutableSet<NSNumber *> *newStyles = [currentActiveStyles mutableCopy];
[newStyles minusSet: previousActiveStyles];
[newStyles unionSet: stylesToBeReAdded];
NSArray<NSNumber*> *sortedNewStyles = [newStyles sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"intValue" ascending:YES]]];
// append opening tags
for(NSNumber *style in sortedNewStyles) {
NSString *tagContent = [self tagContentForStyle:style openingTag:YES location:currentRange.location];
[result appendString: [NSString stringWithFormat:@"<%@>", tagContent]];
}
// append the letter
[result appendString:currentCharacterStr];
// save current styles for next character's checks
previousActiveStyles = currentActiveStyles;
}
// set last character
lastCharacter = currentCharacterChar;
}
if(![[NSCharacterSet newlineCharacterSet] characterIsMember:lastCharacter]) {
// not-newline character was last - finish the paragraph
// close all pending tags
NSArray<NSNumber*> *sortedEndedStyles = [previousActiveStyles sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"intValue" ascending:NO]]];
// append closing tags
for(NSNumber *style in sortedEndedStyles) {
NSString *tagContent = [self tagContentForStyle:style openingTag:NO location:_editor->textView.textStorage.string.length - 1];
[result appendString: [NSString stringWithFormat:@"</%@>", tagContent]];
}
// finish the paragraph
// handle ending of some paragraph styles
if([previousActiveStyles containsObject:@([UnorderedListStyle getStyleType])]) {
[result appendString:@"\n</ul>"];
} else if([previousActiveStyles containsObject:@([OrderedListStyle getStyleType])]) {
[result appendString:@"\n</ol>"];
} else if([previousActiveStyles containsObject:@([BlockQuoteStyle getStyleType])]) {
[result appendString:@"\n</blockquote>"];
} else if(
[previousActiveStyles containsObject:@([H1Style getStyleType])] ||
[previousActiveStyles containsObject:@([H2Style getStyleType])] ||
[previousActiveStyles containsObject:@([H3Style getStyleType])]
) {
// do nothing, heading closing tag has already ben appended
} else {
[result appendString:@"</p>"];
}
} else {
// newline character was last - some paragraph styles need to be closed
if(inUnorderedList) {
inUnorderedList = NO;
[result appendString:@"\n</ul>"];
}
if(inOrderedList) {
inOrderedList = NO;
[result appendString:@"\n</ol>"];
}
if(inBlockQuote) {
inBlockQuote = NO;
[result appendString:@"\n</blockquote>"];
}
}
[result appendString: @"\n</html>"];
return result;
}
- (NSString *)tagContentForStyle:(NSNumber *)style openingTag:(BOOL)openingTag location:(NSInteger)location {
if([style isEqualToNumber: @([BoldStyle getStyleType])]) {
return @"b";
} else if([style isEqualToNumber: @([ItalicStyle getStyleType])]) {
return @"i";
} else if([style isEqualToNumber: @([UnderlineStyle getStyleType])]) {
return @"u";
} else if([style isEqualToNumber: @([StrikethroughStyle getStyleType])]) {
return @"s";
} else if([style isEqualToNumber: @([InlineCodeStyle getStyleType])]) {
return @"code";
} else if([style isEqualToNumber: @([LinkStyle getStyleType])]) {
if(openingTag) {
LinkStyle *linkStyle = (LinkStyle *)_editor->stylesDict[@([LinkStyle getStyleType])];
if(linkStyle != nullptr) {
LinkData *data = [linkStyle getLinkDataAt: location];
if(data != nullptr && data.url != nullptr) {
return [NSString stringWithFormat:@"a href=\"%@\"", data.url];
}
}
return @"a";
} else {
return @"a";
}
} else if([style isEqualToNumber: @([MentionStyle getStyleType])]) {
if(openingTag) {
MentionStyle *mentionStyle = (MentionStyle *)_editor->stylesDict[@([MentionStyle getStyleType])];
if(mentionStyle != nullptr) {
MentionParams *params = [mentionStyle getMentionParamsAt:location];
// attributes can theoretically be nullptr
if(params != nullptr && params.indicator != nullptr && params.text != nullptr) {
NSMutableString *attrsStr = [[NSMutableString alloc] initWithString: @""];
if(params.attributes != nullptr) {
// turn attributes to Data and then into dict
NSData *attrsData = [params.attributes dataUsingEncoding:NSUTF8StringEncoding];
NSError *jsonError;
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:attrsData
options:0
error:&jsonError
];
// format dict keys and values into string
[json enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
[attrsStr appendString: [NSString stringWithFormat:@" %@=\"%@\"", (NSString *)key, (NSString *)obj]];
}];
}
return [NSString stringWithFormat:@"mention text=\"%@\" indicator=\"%@\"%@", params.text, params.indicator, attrsStr];
}
}
return @"mention";
} else {
return @"mention";
}
} else if([style isEqualToNumber:@([H1Style getStyleType])]) {
return @"h1";
} else if([style isEqualToNumber:@([H2Style getStyleType])]) {
return @"h2";
} else if([style isEqualToNumber:@([H3Style getStyleType])]) {
return @"h3";
} else if([style isEqualToNumber:@([UnorderedListStyle getStyleType])] || [style isEqualToNumber:@([OrderedListStyle getStyleType])]) {
return @"li";
} else if([style isEqualToNumber:@([BlockQuoteStyle getStyleType])]) {
// blockquotes use <p> tags the same way lists use <li>
return @"p";
}
return @"";
}
- (void)replaceWholeFromHtml:(NSString * _Nonnull)html {
NSArray *processingResult = [self getTextAndStylesFromHtml:html];
NSString *plainText = (NSString *)processingResult[0];
NSArray *stylesInfo = (NSArray *)processingResult[1];
// reset the text first and reset typing attributes
_editor->textView.text = @"";
_editor->textView.typingAttributes = _editor->defaultTypingAttributes;
// set new text
_editor->textView.text = plainText;
// re-apply the styles
[self applyProcessedStyles:stylesInfo offsetFromBeginning:0];
// run the editor changes callback
[_editor anyTextMayHaveBeenModified];
}
- (void)replaceFromHtml:(NSString * _Nonnull)html range:(NSRange)range {
NSArray *processingResult = [self getTextAndStylesFromHtml:html];
NSString *plainText = (NSString *)processingResult[0];
NSArray *stylesInfo = (NSArray *)processingResult[1];
// we can use ready replace util
[TextInsertionUtils replaceText:plainText inView:_editor->textView at:range additionalAttributes:nil editor:_editor];
[self applyProcessedStyles:stylesInfo offsetFromBeginning:range.location];
[_editor anyTextMayHaveBeenModified];
}
- (void)insertFromHtml:(NSString * _Nonnull)html location:(NSInteger)location {
NSArray *processingResult = [self getTextAndStylesFromHtml:html];
NSString *plainText = (NSString *)processingResult[0];
NSArray *stylesInfo = (NSArray *)processingResult[1];
// same here, insertion utils got our back
[TextInsertionUtils insertText:plainText inView:_editor->textView at:location additionalAttributes:nil editor:_editor];
[self applyProcessedStyles:stylesInfo offsetFromBeginning:location];
[_editor anyTextMayHaveBeenModified];
}
- (void)applyProcessedStyles:(NSArray *)processedStyles offsetFromBeginning:(NSInteger)offset {
for(NSArray* arr in processedStyles) {
// unwrap all info from processed style
NSNumber *styleType = (NSNumber *)arr[0];
StylePair *stylePair = (StylePair *)arr[1];
id<BaseStyleProtocol> baseStyle = _editor->stylesDict[styleType];
// range must be taking offest into consideration because processed styles' ranges are relative to only the new text
// while we need absolute ranges relative to the whole existing text
NSRange styleRange = NSMakeRange(offset + [stylePair.rangeValue rangeValue].location, [stylePair.rangeValue rangeValue].length);
// of course any changes here need to take blocks and conflicts into consideration
if([_editor handleStyleBlocksAndConflicts:[[baseStyle class] getStyleType] range:styleRange]) {
if([styleType isEqualToNumber: @([LinkStyle getStyleType])]) {
NSString *text = [_editor->textView.textStorage.string substringWithRange:styleRange];
NSString *url = (NSString *)stylePair.styleValue;
BOOL isManual = ![text isEqualToString:url];
[((LinkStyle *)baseStyle) addLink:text url:url range:styleRange manual:isManual];
} else if([styleType isEqualToNumber: @([MentionStyle getStyleType])]) {
MentionParams *params = (MentionParams *)stylePair.styleValue;
[((MentionStyle *)baseStyle) addMentionAtRange:styleRange params:params];
} else {
[baseStyle addAttributes:styleRange];
}
}
}
}
- (NSString * _Nullable)initiallyProcessHtml:(NSString * _Nonnull)html {
NSString *fixedHtml = nullptr;
if(html.length >= 15) {
NSString *firstSix = [html substringWithRange:NSMakeRange(0, 6)];
NSString *lastSeven = [html substringWithRange:NSMakeRange(html.length-7, 7)];
NSInteger newlinesCount = [[html componentsSeparatedByString:@"\n"] count] - 1;
if([firstSix isEqualToString:@"<html>"] && [lastSeven isEqualToString:@"</html>"]) {
if(newlinesCount >= 2) {
// looks like our format
// we want to get the string without <html> and </html> and their newlines
// so we skip first 7 characters and get the string 7+8 = 15 characters shorter
fixedHtml = [html substringWithRange: NSMakeRange(7, html.length - 15)];
} else {
// most likely a valid html but with some newline differences
fixedHtml = [html copy];
// firstly remove newlined html tags if any:
fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"<html>\n" withString:@""];
fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"\n</html>" withString:@""];
// fallback; remove html tags without their newlines
fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"<html>" withString:@""];
fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"</html>" withString:@""];
}
} else {
// in other case we are most likely working with some external html - try getting the styles from between body tags
NSRange openingBodyRange = [html rangeOfString:@"<body>"];
NSRange closingBodyRange = [html rangeOfString:@"</body>"];
if(openingBodyRange.length != 0 && closingBodyRange.length != 0) {
NSInteger newStart = openingBodyRange.location + 7;
NSInteger newEnd = closingBodyRange.location - 1;
fixedHtml = [html substringWithRange:NSMakeRange(newStart, newEnd - newStart + 1)];
}
}
}
// second processing - try fixing htmls with wrong newlines' setup
if(fixedHtml != nullptr) {
NSInteger newlinesCount = [[fixedHtml componentsSeparatedByString:@"/n"] count] - 1;
if(newlinesCount == 0) {
// add <br> tag wherever needed
fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"<p></p>" withString:@"<br>"];
// remove <p> tags inside of <li>
fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"<li><p>" withString:@"<li>"];
fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"</p></li>" withString:@"</li>"];
// tags that have to be in separate lines
fixedHtml = [self stringByAddingNewlinesToTag:@"<br>" inString:fixedHtml leading:YES trailing:YES];
fixedHtml = [self stringByAddingNewlinesToTag:@"<ul>" inString:fixedHtml leading:YES trailing:YES];
fixedHtml = [self stringByAddingNewlinesToTag:@"</ul>" inString:fixedHtml leading:YES trailing:YES];
fixedHtml = [self stringByAddingNewlinesToTag:@"<ol>" inString:fixedHtml leading:YES trailing:YES];
fixedHtml = [self stringByAddingNewlinesToTag:@"</ol>" inString:fixedHtml leading:YES trailing:YES];
fixedHtml = [self stringByAddingNewlinesToTag:@"<blockquote>" inString:fixedHtml leading:YES trailing:YES];
fixedHtml = [self stringByAddingNewlinesToTag:@"</blockquote>" inString:fixedHtml leading:YES trailing:YES];
// line opening tags
fixedHtml = [self stringByAddingNewlinesToTag:@"<p>" inString:fixedHtml leading:YES trailing:NO];
fixedHtml = [self stringByAddingNewlinesToTag:@"<li>" inString:fixedHtml leading:YES trailing:NO];
fixedHtml = [self stringByAddingNewlinesToTag:@"<h1>" inString:fixedHtml leading:YES trailing:NO];
fixedHtml = [self stringByAddingNewlinesToTag:@"<h2>" inString:fixedHtml leading:YES trailing:NO];
fixedHtml = [self stringByAddingNewlinesToTag:@"<h3>" inString:fixedHtml leading:YES trailing:NO];
// line closing tags
fixedHtml = [self stringByAddingNewlinesToTag:@"</p>" inString:fixedHtml leading:NO trailing:YES];
fixedHtml = [self stringByAddingNewlinesToTag:@"</li>" inString:fixedHtml leading:NO trailing:YES];
fixedHtml = [self stringByAddingNewlinesToTag:@"</h1>" inString:fixedHtml leading:NO trailing:YES];
fixedHtml = [self stringByAddingNewlinesToTag:@"</h2>" inString:fixedHtml leading:NO trailing:YES];
fixedHtml = [self stringByAddingNewlinesToTag:@"</h3>" inString:fixedHtml leading:NO trailing:YES];
}
}
return fixedHtml;
}
- (NSString *)stringByAddingNewlinesToTag:(NSString *)tag inString:(NSString *)html leading:(BOOL)leading trailing:(BOOL)trailing {
NSString *str = [html copy];
if(leading) {
NSString *formattedTag = [NSString stringWithFormat:@">%@", tag];
NSString *formattedNewTag = [NSString stringWithFormat:@">\n%@", tag];
str = [str stringByReplacingOccurrencesOfString:formattedTag withString:formattedNewTag];
}
if(trailing) {
NSString *formattedTag = [NSString stringWithFormat:@"%@<", tag];
NSString *formattedNewTag = [NSString stringWithFormat:@"%@\n<", tag];
str = [str stringByReplacingOccurrencesOfString:formattedTag withString:formattedNewTag];
}
return str;
}
- (NSArray *)getTextAndStylesFromHtml:(NSString *)fixedHtml {
NSMutableString *plainText = [[NSMutableString alloc] initWithString: @""];
NSMutableDictionary *ongoingTags = [[NSMutableDictionary alloc] init];
NSMutableArray *initiallyProcessedTags = [[NSMutableArray alloc] init];
BOOL insideTag = NO;
BOOL gettingTagName = NO;
BOOL gettingTagParams = NO;
BOOL closingTag = NO;
NSMutableString *currentTagName = [[NSMutableString alloc] initWithString:@""];
NSMutableString *currentTagParams = [[NSMutableString alloc] initWithString:@""];
// firstly, extract text and initially processed tags
for(int i = 0; i < fixedHtml.length; i++) {
NSString *currentCharacterStr = [fixedHtml substringWithRange:NSMakeRange(i, 1)];
unichar currentCharacterChar = [fixedHtml characterAtIndex:i];
if(currentCharacterChar == '<') {
// opening the tag, mark that we are inside and getting its name
insideTag = YES;
gettingTagName = YES;
} else if(currentCharacterChar == '>') {
// finishing some tag, no longer marked as inside or getting its name/params
insideTag = NO;
gettingTagName = NO;
gettingTagParams = NO;
if([currentTagName isEqualToString:@"p"] || [currentTagName isEqualToString:@"br"] || [currentTagName isEqualToString:@"li"]) {
// do nothing, we don't include these tags in styles
} else if(!closingTag) {
// we finish opening tag - get its location and optionally params and put them under tag name key in ongoingTags
NSMutableArray *tagArr = [[NSMutableArray alloc] init];
[tagArr addObject:[NSNumber numberWithInt:plainText.length]];
if(currentTagParams.length > 0) {
[tagArr addObject:[currentTagParams copy]];
}
ongoingTags[currentTagName] = tagArr;
// skip one newline after opening tags that are in separate lines intentionally
if([currentTagName isEqualToString:@"ul"] || [currentTagName isEqualToString:@"ol"] || [currentTagName isEqualToString:@"blockquote"]) {
i += 1;
}
} else {
// we finish closing tags - pack tag name, tag range and optionally tag params into an entry that goes inside initiallyProcessedTags
// skip one newline that was added before some closing tags that are in separate lines
if([currentTagName isEqualToString:@"ul"] || [currentTagName isEqualToString:@"ol"] || [currentTagName isEqualToString:@"blockquote"]) {
plainText = [[plainText substringWithRange: NSMakeRange(0, plainText.length - 1)] mutableCopy];
}
NSMutableArray *tagEntry = [[NSMutableArray alloc] init];
NSArray *tagData = ongoingTags[currentTagName];
NSInteger tagLocation = [((NSNumber *)tagData[0]) intValue];
NSRange tagRange = NSMakeRange(tagLocation, plainText.length - tagLocation);
[tagEntry addObject:[currentTagName copy]];
[tagEntry addObject:[NSValue valueWithRange:tagRange]];
if(tagData.count > 1) {
[tagEntry addObject:[(NSString *)tagData[1] copy]];
}
[initiallyProcessedTags addObject:tagEntry];
[ongoingTags removeObjectForKey:currentTagName];
}
// post-tag cleanup
closingTag = NO;
currentTagName = [[NSMutableString alloc] initWithString:@""];
currentTagParams = [[NSMutableString alloc] initWithString:@""];
} else {
if(!insideTag) {
// no tags logic - just append text
[plainText appendString:currentCharacterStr];
} else {
if(gettingTagName) {
if(currentCharacterChar == ' ') {
// no longer getting tag name - switch to params
gettingTagName = NO;
gettingTagParams = YES;
} else if(currentCharacterChar == '/') {
// mark that the tag is closing
closingTag = YES;
} else {
// append next tag char
[currentTagName appendString:currentCharacterStr];
}
} else if(gettingTagParams) {
// append next tag params char
[currentTagParams appendString:currentCharacterStr];
}
}
}
}
// process tags into proper StyleType + StylePair values
NSMutableArray *processedStyles = [[NSMutableArray alloc] init];
for(NSArray* arr in initiallyProcessedTags) {
NSString *tagName = (NSString *)arr[0];
NSValue *tagRangeValue = (NSValue *)arr[1];
NSMutableString *params = [[NSMutableString alloc] initWithString:@""];
if(arr.count > 2) {
[params appendString:(NSString *)arr[2]];
}
NSMutableArray *styleArr = [[NSMutableArray alloc] init];
StylePair *stylePair = [[StylePair alloc] init];
if([tagName isEqualToString:@"b"]) {
[styleArr addObject:@([BoldStyle getStyleType])];
} else if([tagName isEqualToString:@"i"]) {
[styleArr addObject:@([ItalicStyle getStyleType])];
} else if([tagName isEqualToString:@"u"]) {
[styleArr addObject:@([UnderlineStyle getStyleType])];
} else if([tagName isEqualToString:@"s"]) {
[styleArr addObject:@([StrikethroughStyle getStyleType])];
} else if([tagName isEqualToString:@"code"]) {
[styleArr addObject:@([InlineCodeStyle getStyleType])];
} else if([tagName isEqualToString:@"a"]) {
[styleArr addObject:@([LinkStyle getStyleType])];
// cut only the url from the href="..." string
NSString *url = [params substringWithRange:NSMakeRange(6, params.length - 7)];
stylePair.styleValue = url;
} else if([tagName isEqualToString:@"mention"]) {
[styleArr addObject:@([MentionStyle getStyleType])];
// extract html expression into dict using some regex
NSMutableDictionary *paramsDict = [[NSMutableDictionary alloc] init];
NSString *pattern = @"(\\w+)=\"([^\"]*)\"";
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:nil];
[regex enumerateMatchesInString:params options:0 range:NSMakeRange(0,params.length)
usingBlock:^(NSTextCheckingResult * _Nullable result, NSMatchingFlags flags, BOOL * _Nonnull stop) {
if(result.numberOfRanges == 3) {
NSString *key = [params substringWithRange:[result rangeAtIndex:1]];
NSString *value = [params substringWithRange:[result rangeAtIndex:2]];
paramsDict[key] = value;
}
}
];
MentionParams *mentionParams = [[MentionParams alloc] init];
mentionParams.text = paramsDict[@"text"];
mentionParams.indicator = paramsDict[@"indicator"];
[paramsDict removeObjectsForKeys:@[@"text", @"indicator"]];
NSError *error;
NSData *attrsData = [NSJSONSerialization dataWithJSONObject:paramsDict options:0 error:&error];
NSString *formattedAttrsString = [[NSString alloc] initWithData:attrsData encoding:NSUTF8StringEncoding];
mentionParams.attributes = formattedAttrsString;
stylePair.styleValue = mentionParams;
} else if([[tagName substringWithRange:NSMakeRange(0, 1)] isEqualToString: @"h"]) {
if([tagName isEqualToString:@"h1"]) {
[styleArr addObject:@([H1Style getStyleType])];
} else if([tagName isEqualToString:@"h2"]) {
[styleArr addObject:@([H2Style getStyleType])];
} else if([tagName isEqualToString:@"h3"]) {
[styleArr addObject:@([H3Style getStyleType])];
}
} else if([tagName isEqualToString:@"ul"]) {
[styleArr addObject:@([UnorderedListStyle getStyleType])];
} else if([tagName isEqualToString:@"ol"]) {
[styleArr addObject:@([OrderedListStyle getStyleType])];
} else if([tagName isEqualToString:@"blockquote"]) {
[styleArr addObject:@([BlockQuoteStyle getStyleType])];
} else {
// some other external tags like span just don't get put into the processed styles
continue;
}
stylePair.rangeValue = tagRangeValue;
[styleArr addObject:stylePair];
[processedStyles addObject:styleArr];
}
return @[plainText, processedStyles];
}
@end