我想将NSAttributedString转换为html,如下所示:
This is a <i>string</i> with some <b>simple</b> <i><b>html</b></i> tags in it.
不幸的是,如果您使用Apple的内置系统,它将生成基于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:,特别是“ 讨论” 部分,其中指出:
enumerateAttribute:inRange:options:usingBlock:
如果将此方法发送到NSMutableAttributedString的实例,则允许其突变(删除,添加或更改),只要它在提供给该块的范围内即可;突变后,在针对突变调整了加工范围的长度之后,枚举将继续紧接在加工范围之后的范围。(枚举器基本上假定长度的任何变化都发生在指定范围内。)例如,如果调用的块的范围从位置N开始,并且该块删除了所提供范围内的所有字符,则下一次调用也将传递N作为范围的索引。
换句话说,在闭包/块中,使用range,您可以在那里删除/替换字符。操作系统将在范围的末端放置一个标记。进行修改后,它将计算标记的新范围,以便下一次枚举迭代将从该新标记开始。因此,您不必将所有范围都保留在一个数组中,而是通过向后替换而不修改范围来应用更改。不用为此烦恼,方法已经做到了。
range
我不是Swift开发人员,而是Objective-C开发人员。因此我的Swift代码可能不遵守所有的“ Swift规则”,并且可能有点丑陋(可选,包装等处理不当,if let未完成等)。
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 <i>ITALIC</i> with some <b>BOLD</b> <b><i>BOLDandITALIC</b></i> <b>BOLD<u>UNDERLINEandBOLD</b>RESTUNDERLINE</u> in it."
"This is <i>ITALIC</i> with some <b>BOLD</b> <b><i>BOLDandITALIC</b></i> <b>BOLD<u>UNDERLINEandBOLD</b>RESTUNDERLINE</u> in it."
解决方案带来了两种方法:一种执行一个循环,另一种执行多个循环,但是对于混合标签,结果可能很奇怪。与之前提供的样本检查不同的渲染。