将属性字符串转换为 "simple" 标记 html

Convert attributed string, to, "simple" tagged html

我想将 NSAttributedString 转换为 html,如下所示:

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

不幸的是,如果您使用苹果的内置系统,它会生成基于 css 的冗长 html。 (以下示例供参考。)

那么如何从 NSAttributedString 生成简单标记的 html?

我为此编写了一个非常冗长、脆弱的调用,这是一个糟糕的解决方案。

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
}

下面是使用 Apple 内置系统的方法,不幸的是,它会生成完整的 CSS 等

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)

示例输出....

<!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>

根据 enumerateAttribute:inRange:options:usingBlock: 的文档,尤其是 Discussion 部分指出:

If this method is sent to an instance of NSMutableAttributedString, mutation (deletion, addition, or change) is allowed, as long as it is within the range provided to the block; after a mutation, the enumeration continues with the range immediately following the processed range, after the length of the processed range is adjusted for the mutation. (The enumerator basically assumes any change in length occurs in the specified range.) For example, if block is called with a range starting at location N, and the block deletes all the characters in the supplied range, the next call will also pass N as the index of the range.

换句话说,在closure/block中,用range,你可以在那里delete/replace个字符。 OS 将在范围的那一端放置一个标记。完成修改后,它将计算标记的新范围,以便枚举的下一次迭代将从该新标记开始。 因此,您不必将所有范围都保留在一个数组中,然后通过向后替换而不修改范围来应用更改。不要打扰你,这些方法已经做到了。

我不是 Swift 开发人员,我更像是 Objective-C 开发人员。所以我的 Swift 代码可能不尊重所有 "Swift rules",并且可能有点难看(可选、包装等做得不好,if let 没有完成,等等)

这是我的解决方案:

func attrStrSimpleTag() -> Void {

    let htmlStr = "<!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>"
    let attr = try! NSMutableAttributedString.init(data: htmlStr.data(using: .utf8)!,
                                                   options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType],
                                                   documentAttributes: nil)
    print("Attr: \(attr)")
    attr.enumerateAttribute(NSFontAttributeName, in: NSRange.init(location: 0, length: attr.length), options: []) { (value, range, stop) in
        if let font = value as? UIFont {
            print("font found:\(font)")
            let isBold = font.fontDescriptor.symbolicTraits.contains(.traitBold)
            let isItalic = font.fontDescriptor.symbolicTraits.contains(.traitItalic)
            let occurence = attr.attributedSubstring(from: range).string
            let replacement = self.formattedString(initialString: occurence, bold: isBold, italic: isItalic)
            attr.replaceCharacters(in: range, with: replacement)
        }
    };

    let taggedString = attr.string
    print("taggedString: \(taggedString)")

}

func formattedString(initialString:String, bold: Bool, italic: Bool) -> String {
    var retString = initialString
    if bold {
        retString = "<b>".appending(retString)
        retString.append("</b>")
    }
    if italic
    {
        retString = "<i>".appending(retString)
        retString.append("</i>")
    }

    return retString
}

输出(对于最后一个,其他两个打印仅用于调试):

$> taggedString: So, <i><b>here is</b></i> some stuff

编辑: Objective-C 版本(写得很快,可能有些问题)。

-(void)attrStrSimpleTag
{
    NSString *htmlStr = @"<!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>";
    NSMutableAttributedString *attr = [[NSMutableAttributedString alloc] initWithData:[htmlStr dataUsingEncoding:NSUTF8StringEncoding]
                                                                              options:@{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType}
                                                                   documentAttributes:nil
                                                                                error:nil];
    NSLog(@"Attr: %@", attr);

    [attr enumerateAttribute:NSFontAttributeName inRange:NSMakeRange(0, [attr length]) options:0 usingBlock:^(id  _Nullable value, NSRange range, BOOL * _Nonnull stop) {
        UIFont *font = (UIFont *)value;
        NSLog(@"Font found: %@", font);
        BOOL isBold =  UIFontDescriptorTraitBold & [[font fontDescriptor] symbolicTraits];
        BOOL isItalic =  UIFontDescriptorTraitItalic & [[font fontDescriptor] symbolicTraits];
        NSString *occurence = [[attr attributedSubstringFromRange:range] string];
        NSString *replacement = [self formattedStringWithString:occurence isBold:isBold andItalic:isItalic];
        [attr replaceCharactersInRange:range withString:replacement];
    }];

    NSString *taggedString = [attr string];
    NSLog(@"taggedString: %@", taggedString);
}


-(NSString *)formattedStringWithString:(NSString *)string isBold:(BOOL)isBold andItalic:(BOOL)isItalic
{
    NSString *retString = string;
    if (isBold)
    {
        retString = [NSString stringWithFormat:@"<b>%@</b>", retString];
    }
    if (isItalic)
    {
        retString = [NSString stringWithFormat:@"<i>%@</i>", retString];
    }
    return retString;
}

2020 年 1 月编辑:
更新了更容易修改的代码和 Swift 5,添加了对两种新效果的支持 (underline/strikethrough)。

// MARK: In one loop
extension NSMutableAttributedString {
    func htmlSimpleTagString() -> String {
        enumerateAttributes(in: fullRange(), options: []) { (attributes, range, pointeeStop) in
            let occurence = self.attributedSubstring(from: range).string
            var replacement: String = occurence
            if let font = attributes[.font] as? UIFont {
                replacement = self.font(initialString: replacement, fromFont: font)
            }
            if let underline = attributes[.underlineStyle] as? Int {
                replacement = self.underline(text: replacement, fromStyle: underline)
            }
            if let striked = attributes[.strikethroughStyle] as? Int {
                replacement = self.strikethrough(text: replacement, fromStyle: striked)
            }
            self.replaceCharacters(in: range, with: replacement)
        }
        return self.string
    }
}

// MARK: In multiple loop
extension NSMutableAttributedString {
    func htmlSimpleTagString(options: [NSAttributedString.Key]) -> String {
        if options.contains(.underlineStyle) {
            enumerateAttribute(.underlineStyle, in: fullRange(), options: []) { (value, range, pointeeStop) in
                let occurence = self.attributedSubstring(from: range).string
                guard let style = value as? Int else { return }
                if NSUnderlineStyle(rawValue: style) == NSUnderlineStyle.styleSingle {
                    let replacement = self.underline(text: occurence, fromStyle: style)
                    self.replaceCharacters(in: range, with: replacement)
                }
            }
        }
        if options.contains(.strikethroughStyle) {
            enumerateAttribute(.strikethroughStyle, in: fullRange(), options: []) { (value, range, pointeeStop) in
                let occurence = self.attributedSubstring(from: range).string
                guard let style = value as? Int else { return }
                let replacement = self.strikethrough(text: occurence, fromStyle: style)
                self.replaceCharacters(in: range, with: replacement)
            }
        }
        if options.contains(.font) {
            enumerateAttribute(.font, in: fullRange(), options: []) { (value, range, pointeeStop) in
                let occurence = self.attributedSubstring(from: range).string
                guard let font = value as? UIFont else { return }
                let replacement = self.font(initialString: occurence, fromFont: font)
                self.replaceCharacters(in: range, with: replacement)
            }
        }
        return self.string

    }
}

//MARK: Replacing
extension NSMutableAttributedString {

    func font(initialString: String, fromFont font: UIFont) -> String {
        let isBold = font.fontDescriptor.symbolicTraits.contains(.traitBold)
        let isItalic = font.fontDescriptor.symbolicTraits.contains(.traitItalic)
        var retString = initialString
        if isBold {
            retString = "<b>" + retString + "</b>"
        }
        if isItalic {
            retString = "<i>" + retString + "</i>"
        }
        return retString
    }

    func underline(text: String, fromStyle style: Int) -> String {
        return "<u>" + text + "</u>"
    }

    func strikethrough(text: String, fromStyle style: Int) -> String {
        return "<s>" + text + "</s>"
    }
}

//MARK: Utility
extension NSAttributedString {
    func fullRange() -> NSRange {
        return NSRange(location: 0, length: self.length)
    }
}

使用混合标签进行简单 HTML 测试:"This is <i>ITALIC</i> with some <b>BOLD</b> <b><i>BOLDandITALIC</b></i> <b>BOLD<u>UNDERLINEandBOLD</b>RESTUNDERLINE</u> in it."

解决方案带来了两种方法:一种做一个循环,另一种做多个循环,但是对于混合标签,结果可能很奇怪。检查之前提供的示例不同的渲染。

我有很好的方法将 NSAttributedString 转换成简单的 HTML 字符串。

1)取UIWebViewUITextView.

2) 在 WebView.

中设置属性字符串
[webView loadHTMLString:[yourAttributedString stringByReplacingOccurrencesOfString:@"\n" withString:@"<br/>"] baseURL:nil];

3) 从 UIWebView.

中获取 HTML 字符串
NSString *simpleHtmlString = [webView stringByEvaluatingJavaScriptFromString:@"document.body.innerHTML"];

这里有一个更完整的解决方案,可以保留更多样式和链接。

如果您想保留颜色和字距调整信息,请查看 NSAttributedString.h 中的其他键。

@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

编辑: 在处理 URL.

时,我已将您需要检查的条件添加到 enable/disable 下划线检测