why?
主要是苹果自带的HTML和富文本相互转换不好用,如:和安卓端不互通(斜体、下划线...),转出来和H5也有一定的差异
在客户端写这个功能,还不如直接使用web的编辑器,没差异,而且还快
进度
一、已完成
- 纯文字的富文本转HTML
- HTML转富文本
二、待完成【有人需要再搞】
- 带图片的富文本转HTML
- 异步实现转换【目前主线程实现,会有卡顿】
- ......
代码如下:
import UIKit
fileprivate let color_text = "color"
fileprivate let fontSize_text = "font-size"
class HTMLParser {
/// 富文本转HTML
/// - Parameter attributedText: 富文本
/// - Returns: HTML字符串
class func htmlString(with attributedText: NSAttributedString) -> String {
if attributedText.length == 0 {
return ""
}
var htmlString = ""
var effectiveRange = NSRange(location: 0, length: 0)
let html_p_end = HTML.p_end.label
let html_span_end = HTML.span_end.label
var temp_html_style : String = ""
var temp_html_p : String = ""
// 遍历富文本参数
while effectiveRange.location + effectiveRange.length < attributedText.string.count {
// 获取样式字典
let attributes = attributedText.attributes(at: effectiveRange.location + effectiveRange.length, effectiveRange: &effectiveRange)
// 替换空格,获取text
let string = HTML.space(text: (attributedText.string as NSString).substring(with: effectiveRange)).label
// html 样式
let htmlStyleInfo = HTMLInfo(attributes: attributes)
// 段落
let html_p = HTML.p(htmlStyleInfo.text_align).label
let html_span = HTML.span(htmlStyleInfo.styles).label
let html_text = HTML.tag(htmlStyleInfo.tags, text: string).label
/*
开始拼接 HTML 标签
*/
// 拼接第一个<p>标签
if htmlString.isEmpty {
htmlString.append(html_p)
temp_html_p = html_p
}
if (temp_html_style.count > 0 && temp_html_style != html_span) || temp_html_p != html_p{
htmlString.append(html_span_end)
}
if temp_html_p != html_p {
htmlString.append(html_p_end)
}
if !string.isEmpty {
var haveNewP = false
if htmlString.hasSuffix(html_p_end){
htmlString.append(html_p)
temp_html_p = html_p
haveNewP = true
}
if temp_html_style != html_span || haveNewP{
htmlString.append(html_span)
temp_html_style = html_span
}
htmlString.append(HTML.br(htmlString: html_text).label)
}
}
if !htmlString.isEmpty{
htmlString.append(html_span_end)
htmlString.append(html_p_end)
}
return htmlString
}
/// HTML转富文本
/// - Parameter htmlString: HTML字符串
/// - Returns: 富文本
class func attributedText(htmlString: String) -> NSAttributedString? {
let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [
.documentType: NSAttributedString.DocumentType.html,
.characterEncoding: String.Encoding.utf8.rawValue
]
guard let data = htmlString.data(using: .utf8),
let attributedString = try? NSAttributedString(data: data, options: options, documentAttributes: nil) else {
return nil
}
var mutableAttributedString = replaceSpecialCharacters(in: attributedString).mutableCopy() as! NSMutableAttributedString
// 处理换行符
let range = NSRange(location: mutableAttributedString.length - 1, length: 1)
if mutableAttributedString.string.hasSuffix("\n") {
mutableAttributedString.replaceCharacters(in: range, with: "")
}
// 修正字体 font-family: "Times New Roman" --> 系统字体
mutableAttributedString.enumerateAttribute(.font, in: NSRange(location: 0, length: mutableAttributedString.length), options: .longestEffectiveRangeNotRequired) { value, range, _ in
if let font = value as? UIFont {
let italic = font.fontType == .italic || font.fontType == .boldItalic
let bold = font.fontType == .bold || font.fontType == .boldItalic
mutableAttributedString.addAttribute(.font, value: font.setItalic(italic, bold: bold), range: range)
}
}
// 设置行高
mutableAttributedString = attrubuteStringSetLineHeight(mutableAttributedString, lineHeight: 27.0)
return mutableAttributedString.copy() as? NSAttributedString
}
/// 判断是否有一级二级标题
class func hasTitleFont(with attributedText: NSAttributedString, range: NSRange) -> Bool {
var effectiveRange = NSRange(location: range.location, length: 0)
while effectiveRange.location + effectiveRange.length < NSMaxRange(range) {
let attributes = attributedText.attributes(at: effectiveRange.location + effectiveRange.length, effectiveRange: &effectiveRange)
if let font = attributes[NSAttributedString.Key.font] as? UIFont {
// 字号
let fontSize = font.fontDescriptor.fontAttributes[UIFontDescriptor.AttributeName.size] as? CGFloat ?? 0.0
if fontSize >= 22 {
return true
}
}
}
return false
}
class func attrubuteStringSetButeType(_ attributedText: NSAttributedString, range: NSRange, typeAttributes: [NSAttributedString.Key: Any]) -> NSMutableAttributedString {
let mAttributedString = attributedText.mutableCopy() as! NSMutableAttributedString
var effectiveRange = NSRange(location: range.location, length: 0)
while effectiveRange.location + effectiveRange.length < NSMaxRange(range) {
var attributes = attributedText.attributes(at: effectiveRange.location + effectiveRange.length, effectiveRange: &effectiveRange)
// 对齐方式
if let paragraphStyle = typeAttributes[NSAttributedString.Key.paragraphStyle] as? NSParagraphStyle {
attributes[NSAttributedString.Key.paragraphStyle] = paragraphStyle
}
// 斜体
if let obliqueness = typeAttributes[NSAttributedString.Key.obliqueness] as? NSNumber {
let originalFont = attributes[NSAttributedString.Key.font] as? UIFont ?? UIFont.systemFont(ofSize: 19)
let bold = originalFont.fontType == .bold || originalFont.fontType == .boldItalic
attributes[NSAttributedString.Key.font] = originalFont.setItalic(Float(truncating: obliqueness) > 0, bold: bold)
}
// 颜色
if let foregroundColor = typeAttributes[NSAttributedString.Key.foregroundColor] as? UIColor {
attributes[NSAttributedString.Key.foregroundColor] = foregroundColor
}
// 字体
if let font = typeAttributes[NSAttributedString.Key.font] as? UIFont {
let originalFont = attributes[NSAttributedString.Key.font] as? UIFont ?? font
let bold = font.fontType == .bold || font.fontType == .boldItalic
let italic = originalFont.fontType == .italic || originalFont.fontType == .boldItalic
attributes[NSAttributedString.Key.font] = font.setItalic(italic, bold: bold)
}
// 下划线
if let underlineStyle = typeAttributes[NSAttributedString.Key.underlineStyle] as? NSNumber {
attributes[NSAttributedString.Key.underlineStyle] = underlineStyle
}
// 删除线
if let strikethroughStyle = typeAttributes[NSAttributedString.Key.strikethroughStyle] as? NSNumber {
attributes[NSAttributedString.Key.strikethroughStyle] = strikethroughStyle
}
mAttributedString.addAttributes(attributes, range: NSIntersectionRange(effectiveRange, range))
}
return mAttributedString
}
// MARK: - private
private class func attrubuteStringSetLineHeight(_ attributedText: NSAttributedString, lineHeight: CGFloat) -> NSMutableAttributedString {
let mAttributedString = attributedText.mutableCopy() as! NSMutableAttributedString
var effectiveRange = NSRange(location: 0, length: 0)
while effectiveRange.location + effectiveRange.length < attributedText.string.count {
let attributes = attributedText.attributes(at: effectiveRange.location + effectiveRange.length, effectiveRange: &effectiveRange)
let fontLineHeight = lineHeight
var tempTypingAttributes = [NSAttributedString.Key: Any]()
if let p = attributes[NSAttributedString.Key.paragraphStyle] as? NSMutableParagraphStyle {
let np = NSMutableParagraphStyle()
np.setParagraphStyle(p)
np.minimumLineHeight = fontLineHeight
np.maximumLineHeight = fontLineHeight
tempTypingAttributes[NSAttributedString.Key.paragraphStyle] = np
}
mAttributedString.addAttributes(tempTypingAttributes, range: effectiveRange)
}
return mAttributedString
}
private class func replaceSpecialCharacters(in attributedString: NSAttributedString) -> NSAttributedString {
let mutableAttributedString = attributedString.mutableCopy() as! NSMutableAttributedString
if mutableAttributedString.length <= 0 {return attributedString}
// 定义要替换的特殊字符
let specialCharacter = "\u{2028}"
let newLineCharacter = "\u{2028}\n"
mutableAttributedString.mutableString.replaceOccurrences(of: newLineCharacter, with: specialCharacter, options: .literal, range: NSRange(location: 0, length: mutableAttributedString.length))
if mutableAttributedString.length <= 0 {return attributedString}
// 创建正则表达式
let regex = try! NSRegularExpression(pattern: specialCharacter, options: [])
// 在整个富文本范围内搜索特殊字符
let fullRange = NSRange(location: 0, length: mutableAttributedString.length)
regex.enumerateMatches(in: mutableAttributedString.string, options: [], range: fullRange) { result, _, _ in
if let result = result {
mutableAttributedString.replaceCharacters(in: result.range, with: "\n")
}
}
return mutableAttributedString.copy() as! NSAttributedString
}
}
// MARK: - HTML 样式信息
fileprivate struct HTMLInfo {
/// 字符的 HTML style
struct textStyles {
var color : String
var fontSize : CGFloat
var fontStyle : String
}
var styles : textStyles = textStyles(color: "#1A1A1A", fontSize: 19, fontStyle: "normal")
/// 字符的 HTML tag
var tags = [String]()
/// 段落方向
var text_align = "left"
init(attributes: [NSAttributedString.Key : Any]) {
if let style = attributes[.paragraphStyle] as? NSParagraphStyle {
text_align = getAlignString(style.alignment)
}
let font = attributes[NSAttributedString.Key(rawValue: "NSOriginalFont")] as? UIFont ?? attributes[.font] as? UIFont
if let font = font {
// 字色 16 进制
let textColor = ((attributes[.foregroundColor] as? UIColor) ?? .black).doraemon_HexString()
// 字号
let fontSize = font.fontDescriptor.fontAttributes[.size] as? CGFloat ?? UIFont.systemFontSize
// 字体样式
var fontStyle = "normal"
// 斜体
if font.fontType == .boldItalic || font.fontType == .italic {
fontStyle = "italic"
}
styles = textStyles(color: textColor, fontSize: fontSize, fontStyle: fontStyle)
}
// 粗体
if let traits = (attributes[.font] as? UIFont)?.fontDescriptor.symbolicTraits, (traits.rawValue & UIFontDescriptor.SymbolicTraits.traitBold.rawValue) != 0 {
tags.append("b")
}
// 下划线
if let underline = attributes[.underlineStyle] as? NSNumber, underline != 0 {
tags.append("u")
}
// 删除线
if let strikethrough = attributes[.strikethroughStyle] as? NSNumber, strikethrough != 0 {
tags.append("del")
}
}
/// 段落方向转字符串
private func getAlignString(_ alignment: NSTextAlignment) -> String {
switch alignment {
case .left:
return "left"
case .center:
return "center"
case .right:
return "right"
default:
return "left"
}
}
}
// MARK: - HTML 标签
fileprivate enum HTML {
case p(_ textAlign: String)
case p_end
case span(_ styles: HTMLInfo.textStyles)
case span_end
case tag(_ tags: [String], text: String)
case br(htmlString: String)
case space(text: String)
// 获取HTML标签
var label: String {
switch self {
case .p(let textAlign):
return "<p style=\"text-align:\(textAlign);\">"
case .p_end:
return "</p>"
case .span(let styles):
return "<span style=\"color:\(styles.color);font-size:\(styles.fontSize)px;font-style:\(styles.fontStyle);\">"
case .span_end:
return "</span>"
case .tag(let tags, let text):
var htmlString = ""
tags.forEach { htmlString.append("<\($0)>") }
htmlString.append(text)
tags.forEach { htmlString.append("</\($0)>") }
return htmlString
case .br(let htmlString):
return htmlString.replacingOccurrences(of: "\n", with: "<br>")
case .space(var text):
text = text.replacingOccurrences(of: " ", with: " ")
return text.replacingOccurrences(of: "\u{00A0}", with: " ")
}
}
}
extension UIColor {
func doraemon_HexString() -> String {
let components = self.cgColor.components
if let components = components {
let red = Int(components[0] * 255.0)
let green = Int(components[1] * 255.0)
let blue = Int(components[2] * 255.0)
return String(format: "#%02X%02X%02X", red, green, blue)
} else {
return ""
}
}
}
public extension UIFont {
enum FontType {
case normal
case boldItalic
case bold
case italic
}
/// 字体类型
var fontType: FontType {
let desc = self.description.replacingOccurrences(of: " ", with: "")
let b = desc.contains("font-weight:bold") || desc.contains("font-weight:Bold")
let i = desc.contains("font-style:italic") || desc.contains("font-style:Italic")
if b && i {
return .boldItalic
}
if b { return .bold }
if i { return .italic}
return .normal
}
func setItalic(_ italic: Bool, bold: Bool) -> UIFont{
if italic && bold {
// 粗斜体
return UIFont.init(descriptor: .init(name: ".AppleSystemUIFontBoldItalic", matrix: CGAffineTransform.init(1, 0, 0.2, 1, 0, 0)), size: self.pointSize)
}else if italic {
// 斜体
return UIFont.init(descriptor: .init(name: ".AppleSystemUIFontItalic", matrix: CGAffineTransform.init(1, 0, 0.2, 1, 0, 0)), size: self.pointSize)
}else if bold{
// 粗体
return UIFont.boldSystemFont(ofSize: self.pointSize)
}
// 普通字体
return UIFont.systemFont(ofSize: self.pointSize)
}
}