Convert attributed string, to, "simple" tagged html

Viewed 3656

I want to convert an NSAttributedString, to html like this:

This is a <i>string</i> with some <b>simple</b> <i><b>html</b></i> tags in it.

Unfortunately if you use apple's built-in system it generates verbose css-based html. (Example below for reference.)

So how to generate simple tagged html from an NSAttributedString?

I wrote a very verbose, fragile call to do it, which is a poor solution.

func simpleTagStyle(fromNSAttributedString att: NSAttributedString)->String {

    // verbose, fragile solution

    // essentially, iterate all the attribute ranges in the attString
    // make a note of what style they are, bold italic etc
    // (totally ignore any not of interest to us)
    // then basically get the plain string, and munge it for those ranges.
    // be careful with the annoying "multiple attribute" case
    // (an alternative would be to repeatedly munge out attributed ranges
    // one by one until there are none left.)

    let rangeAll = NSRange(location: 0, length: att.length)

    // make a note of all of the ranges of bold/italic
    // (use a tuple to remember which is which)
    var allBlocks: [(NSRange, String)] = []

    att.enumerateAttribute(
        NSFontAttributeName,
        in: rangeAll,
        options: .longestEffectiveRangeNotRequired
        )
            { value, range, stop in

            handler: if let font = value as? UIFont {

                let b = font.fontDescriptor.symbolicTraits.contains(.traitBold)
                let i = font.fontDescriptor.symbolicTraits.contains(.traitItalic)

                if b && i {
                    allBlocks.append( (range, "bolditalic") )
                    break handler   // take care not to duplicate
                }

                if b {
                    allBlocks.append( (range, "bold") )
                    break handler
                }

                if i {
                    allBlocks.append( (range, "italic") )
                    break handler
                }
            }

        }

    // traverse those backwards and munge away

    var plainString = att.string

    for oneBlock in allBlocks.reversed() {

        let r = oneBlock.0.range(for: plainString)!

        let w = plainString.substring(with: r)

        if oneBlock.1 == "bolditalic" {
            plainString.replaceSubrange(r, with: "<b><i>" + w + "</i></b>")
        }

        if oneBlock.1 == "bold" {
            plainString.replaceSubrange(r, with: "<b>" + w + "</b>")
        }

        if oneBlock.1 == "italic" {
            plainString.replaceSubrange(r, with: "<i>" + w + "</i>")
        }

    }

    return plainString
}

So here's how to use Apple's built in system, which unfortunately generates full-on CSS etc.

x = ... your NSAttributedText
var resultHtmlText = ""
do {

    let r = NSRange(location: 0, length: x.length)
    let att = [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType]

    let d = try x.data(from: r, documentAttributes: att)

    if let h = String(data: d, encoding: .utf8) {
        resultHtmlText = h
    }
}
catch {
    print("utterly failed to convert to html!!! \n>\(x)<\n")
}
print(resultHtmlText)

Example output....

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="Content-Style-Type" content="text/css">
<title></title>
<meta name="Generator" content="Cocoa HTML Writer">
<style type="text/css">
p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 14.0px 'Some Font'}
span.s1 {font-family: 'SomeFont-ItalicOrWhatever'; font-weight: normal; font-style: normal; font-size: 14.00pt}
span.s2 {font-family: 'SomeFont-SemiboldItalic'; font-weight: bold; font-style: italic; font-size: 14.00pt}
</style>
</head>
<body>
<p class="p1"><span class="s1">So, </span><span class="s2">here is</span><span class="s1"> some</span> stuff</p>
</body>
</html>
3 Answers

I have good way to convert NSAttributedString into simple HTML string .

1) Take UIWebView and UITextView.

2) Set your Attributed string in WebView.

[webView loadHTMLString:[yourAttributedString stringByReplacingOccurrencesOfString:@"\n" withString:@"<br/>"] baseURL:nil];

3) Get your HTML string from UIWebView.

NSString *simpleHtmlString = [webView stringByEvaluatingJavaScriptFromString:@"document.body.innerHTML"];

Here's a more complete solution that preserves more styles and links.

Check out the other keys in NSAttributedString.h if you want to preserve color and kerning information.

@implementation NSAttributedString (SimpleHTML)

- (NSString*) simpleHTML
{
    NSMutableAttributedString*  attr = [self mutableCopy];

    [attr enumerateAttributesInRange: NSMakeRange(0, [self length])
                             options: 0
                          usingBlock: ^(NSDictionary<NSAttributedStringKey,id> * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop)
    {
        for (NSString* aKey in attrs.allKeys.copy)
        {
            NSString*   format          =   nil;

            if ([aKey compare: NSFontAttributeName] == NSOrderedSame) //UIFont, default Helvetica(Neue) 12
            {
                UIFont* font = attrs[aKey];

                BOOL isBold =  UIFontDescriptorTraitBold & [[font fontDescriptor] symbolicTraits];
                BOOL isItalic =  UIFontDescriptorTraitItalic & [[font fontDescriptor] symbolicTraits];

                if (isBold && isItalic)
                {
                    format = @"<b><i>%@</i></b>";
                }
                else if (isBold)
                {
                    format = @"<b>%@</b>";
                }
                else if (isItalic)
                {
                    format = @"<i>%@</i>";
                }
            }
            else if ([aKey compare: NSStrikethroughStyleAttributeName] == NSOrderedSame) //NSNumber containing integer, default 0: no strikethrough
            {
                NSNumber*   strike  =   (id) attrs[aKey];

                if (strike.boolValue)
                {
                    format = @"<strike>";
                }
                else
                {
                    format = @"</strike>";
                }
            }
            else if ([aKey compare: NSUnderlineStyleAttributeName] == NSOrderedSame) //NSNumber containing integer, default 0: no underline
            {
            if ([attrs.allKeys containsObject: NSLinkAttributeName] == NO)
                {
                    NSNumber*   underline  =   (id) attrs[aKey];

                    if (underline.boolValue)
                    {
                        format = @"<u>%@</u>";
                    }
                }
            }
            else if ([aKey compare: NSLinkAttributeName] == NSOrderedSame) //NSURL (preferred) or NSString
            {
                NSObject*   value       =   (id) attrs[aKey];
                NSString*   absolute    =   @"";

                if ([value isKindOfClass: NSURL.class])
                {
                    NSURL*      url =   (id) value;

                    absolute = url.absoluteString;
                }
                else if ([value isKindOfClass: NSString.class])
                {
                    absolute = (id) value;
                }

                format = [NSString stringWithFormat: @"<a href=\"%@\">%%@</a>", absolute];
            }

            if (format)
            {
                NSString*   occurence   =   [[attr attributedSubstringFromRange: range] string];
                NSString*   replacement =   [NSString stringWithFormat: format, occurence];

                [attr replaceCharactersInRange: range
                                    withString: replacement];
            }
        }
    }];

    NSMutableString*    result  =   [[NSString stringWithFormat: @"<html>%@</html>", attr.string] mutableCopy];

    [result replaceOccurrencesOfString: @"\n"
                            withString: @"<br>"
                               options: 0
                                 range: NSMakeRange(0, result.length)];

    return result;
}

@end

EDIT: I've added the conditional you need to check to enable/disable underline detection while it's treating a URL.

Related