Search code examples
htmliosobjective-cattributesnsattributedstring

Convert nested bold italic HTML tags to NSAttributedString


I am using this code to convert HTML strings to NSAttributedString using RegEx. The only problem I am having is with nested bold and italic tags. Is RegEx the correct way to do it?

Also I want to avoid using an HTML parser because all I need is Bold, Italic and Underline attributes and StrikeThrough if Possible.

Any suggestions?

- (NSMutableAttributedString *)applyStylesToString:(NSString *)string searchRange:(NSRange)searchRange {

    NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:string];
    [attributedString addAttribute:NSFontAttributeName value:[StylesConfig regularLargeFont] range:searchRange];

    NSDictionary *boldAttributes = @{ NSFontAttributeName : [StylesConfig regularBoldLargeFont] };
    NSDictionary *italicAttributes = @{ NSFontAttributeName : [StylesConfig regularItalicLargeFont] };
    NSDictionary *underlineAttributes = @{ NSUnderlineStyleAttributeName : @1};
    NSDictionary *strikeThroughAttributes = @{ NSStrikethroughStyleAttributeName : @1};

    NSDictionary *replacements = @{
                                   @"<b>(.*?)</b>" : boldAttributes,
                                   @"<i>(.*?)</i>" : italicAttributes,
                                   @"<u>(.*?)</u>" : underlineAttributes,
                                   @"<s>(.*?)</s>" : strikeThroughAttributes
                                   };

    for (NSString *key in replacements) {

        NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:key options:0 error:nil];

        [regex enumerateMatchesInString:string options:0 range:searchRange usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop){
            NSRange matchRange = [match rangeAtIndex:1];

            [attributedString addAttributes:replacements[key] range:matchRange];

            if ([key isEqualToString:@"<b>(.*?)</b>"]) {
                [self makeBoldItalic:attributedString matchRange:matchRange font:@"SourceSansPro-It"];

            } else if ([key isEqualToString:@"<i>(.*?)</i>"]) {
                [self makeBoldItalic:attributedString matchRange:matchRange font:@"SourceSansPro-Semibold"];
            }
        }];
    }
    [[attributedString mutableString] replaceOccurrencesOfString:@"<b>" withString:@"" options:NSCaseInsensitiveSearch range:NSMakeRange(0, attributedString.length)];
    [[attributedString mutableString] replaceOccurrencesOfString:@"</b>" withString:@"" options:NSCaseInsensitiveSearch range:NSMakeRange(0, attributedString.length)];
    [[attributedString mutableString] replaceOccurrencesOfString:@"<i>" withString:@"" options:NSCaseInsensitiveSearch range:NSMakeRange(0, attributedString.length)];
    [[attributedString mutableString] replaceOccurrencesOfString:@"</i>" withString:@"" options:NSCaseInsensitiveSearch range:NSMakeRange(0, attributedString.length)];
    [[attributedString mutableString] replaceOccurrencesOfString:@"<u>" withString:@"" options:NSCaseInsensitiveSearch range:NSMakeRange(0, attributedString.length)];
    [[attributedString mutableString] replaceOccurrencesOfString:@"</u>" withString:@"" options:NSCaseInsensitiveSearch range:NSMakeRange(0, attributedString.length)];
    [[attributedString mutableString] replaceOccurrencesOfString:@"<s>" withString:@"" options:NSCaseInsensitiveSearch range:NSMakeRange(0, attributedString.length)];
    [[attributedString mutableString] replaceOccurrencesOfString:@"</s>" withString:@"" options:NSCaseInsensitiveSearch range:NSMakeRange(0, attributedString.length)];

    return attributedString;
}

- (void)makeBoldItalic:(NSMutableAttributedString *)attributedString matchRange:(NSRange)matchRange font:(NSString *)font {

    [attributedString enumerateAttribute:NSFontAttributeName inRange:NSMakeRange(0, attributedString.length) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) {

        UIFont *oldFont = (UIFont *)value;
        if ([oldFont.fontName isEqualToString:font]) {

            [attributedString removeAttribute:NSFontAttributeName range:range];
            NSDictionary *boldItalicAttributes = @{ NSFontAttributeName : [StylesConfig regularBoldItalicLargeFont] };
            [attributedString addAttributes:boldItalicAttributes range:matchRange];
        }
    }];
}

Solution

  • The thing is that Bold and Italic (and other attributes) are part of the font, unlike underlining. iOS doesn't give a "fake bold" or "fake italic" like Photoshop can do for example.

    So, I guess from your StylesConfig class utilities that you have: boldFont, italicFont and boldItalicFont.

    boldFont = [StylesConfig regularBoldLargeFont]; //SourceSansPro-Semibold
    italicFont = [StylesConfig regularItalicLargeFont];//SourceSansPro-It
    boldItalicFont = [StylesConfig regularBoldItalicLargeFont];//SourceSansPro-SemiboldIt (or something like this)
    

    So to change the font, expecting that the font in its symbolicTraits fontDescriptor contains the correct one, you can do something like that:

    -(void)boldText:(NSMutableAttributedText *)attributeString forRange:(NSRange)range
    {
        [attributedString enumerateAttribute:NSFontAttributeName inRange:NSMakeRange(0, attributedString.length) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) {
        UIFont *currentFont = (UIFont *)value;
        if ([[currentFont fontDescriptor] symbolicTraits] & UIFontDescriptorTraitBold)
        {
            NSLog(@"Font is already bold);
        }
        else
        {
             UIFont *newFont = [self boldFontFromFont:currentFont];
             [attributedString addAttribute:newFont range:range];
        }
        }];
    }
    
    -(void)italicText:(NSMutableAttributedText *)attributeString forRange:(NSRange)range
    {
        [attributedString enumerateAttribute:NSFontAttributeName inRange:NSMakeRange(0, attributedString.length) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) {
        UIFont *currentFont = (UIFont *)value;
        if ([[currentFont fontDescriptor] symbolicTraits] & UIFontDescriptorTraitItalic)
        {
            NSLog(@"Font is already italic);
        }
        else
        {
             UIFont *newFont = [self italicFontFromFont:currentFont];
             [attributedString addAttribute:newFont range:range];
        }
        }];
    }
    
    -(UIFont *)boldFontFromFont:(UIFont *)font
    {
        if ([[currentFont fontDescriptor] symbolicTraits] & UIFontDescriptorTraitItalic)
            return boldItalicFont;
        else
            return boldFont;
    }
    
    -(UIFont *)italicFontFromFont:(UIFont *)font
    {
        if ([[currentFont fontDescriptor] symbolicTraits] & UIFontDescriptorTraitBold)
            return boldItalicFont;
        else
            return italicFont;
    }
    

    In your current code:

    if ([key isEqualToString:@"<b>(.*?)</b>"]) 
    {
        [self boldText: attributedString forRange:matchRange];
    }
     else if ([key isEqualToString:@"<i>(.*?)</i>"]) 
    {
        [self italicText: attributedString forRange:matchRange];
    }
    

    If your font doesn't pass the symbolicTraits tests, you may have to look into the UIFont fontName and check if its Bold, Italic, or BoldItalic.

    Note:
    I didn't test the code, I write it only here, I don't even know if it compiles, but that should give you the whole idea.