【问题标题】:Convert attributed string, to, "simple" tagged html将属性字符串转换为“简单”标记的 html
【发布时间】:2017-10-17 01:57:08
【问题描述】:

我想将 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>

【问题讨论】:

  • 这里有问题吗?
  • 在这里,我将编辑问题,使其更简单
  • 您可能需要重新组织您的问题,因为您根本不清楚您在问什么以及遇到了什么问题。
  • 您需要编写自己的解析器。 html/css 有什么问题?
  • 嘿@LeoDabus sup。在任何情况下,您都需要“标记”普通 html 片段。例如,您的服务器团队需要这种方式。不幸的是,如果您需要“降价”格式,则有很多解决此问题的库,但是(据我所知)对于普通的 old-skool html 标签没有任何作用。很难相信 Swift 中没有比我的手工代码更脆弱的东西。你知道吗?

标签: html swift markdown nsattributedstring


【解决方案1】:

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

如果这个方法被发送到 NSMutableAttributedString 的一个实例, 允许突变(删除、添加或更改),只要它是 在提供给区块的范围内;突变后, 枚举在紧随其后的范围内继续 处理范围,调整处理范围的长度后 为突变。 (枚举器基本上假设任何变化 长度出现在指定的范围内。)例如,如果块被调用 范围从位置 N 开始,并且该块删除所有 提供范围内的字符,下一次调用也将传递 N 作为 范围的索引。

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

我不是 Swift 开发人员,我更像是一名 Objective-C 开发人员。所以我的 Swift 代码可能不遵守所有“Swift 规则”,并且可能有点丑陋(可选、包装等做得不好,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,添加了对两种新效果(下划线/删除线)的支持。

// 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 &lt;i&gt;ITALIC&lt;/i&gt; with some &lt;b&gt;BOLD&lt;/b&gt; &lt;b&gt;&lt;i&gt;BOLDandITALIC&lt;/b&gt;&lt;/i&gt; &lt;b&gt;BOLD&lt;u&gt;UNDERLINEandBOLD&lt;/b&gt;RESTUNDERLINE&lt;/u&gt; in it."

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

【讨论】:

  • 完全惊人的信息 - 难以置信!谢谢拉尔姆!
  • 这段代码有客观的c转换吗?
  • @MilanGupta 我编辑了答案以包含 Objective-C 版本。
  • @Larme 非常感谢。你节省了我很多时间!
【解决方案2】:

我有很好的方法将 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"];

【讨论】:

  • 嗯,使用“stringByEvaluatingJavaScriptFromString”非常聪明!我想知道它是否避免了所有的 CSS ??
  • 是的,直接来自webView。那是我们只得到没有 CSS 的标签。@Fattie
【解决方案3】:

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

如果您想保留颜色和字距调整信息,请查看 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 时需要检查以启用/禁用下划线检测的条件。

【讨论】:

  • [aKey compare: NSStrikethroughStyleAttributeName] == NSOrderedSame:不应该简化成[aKey equalsToString: NSStrikethroughStyleAttributeName]吗?对于下划线,您可能希望针对NSUnderlineStyleNone 进行测试,而不是boolValue(即使它“有点工作”,这是一个奇怪的测试)并且由于存在其他值而具有误导性。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2022-01-19
  • 2012-04-10
  • 1970-01-01
  • 2019-09-15
  • 2021-04-03
  • 2018-02-13
  • 1970-01-01
相关资源
最近更新 更多