ZMarkupParser|Swift HTML String 转 NSAttributedString 工具,支援客制样式与高效能渲染
针对 iOS/macOS 开发者,提供纯 Swift 实作的 HTML String 转 NSAttributedString 工具,支援自动修正错误标签、客制样式设定与多种 HTML Tag,提升渲染效能并避免长字串闪退,助你打造流畅又稳定的文字显示效果。
Click here to view the English version of this article.
點擊這裡查看本文章正體中文版本。
基于 SEO 考量,本文标题与描述经 AI 调整,原始版本请参考内文。
ZMarkupParser HTML String 转换 NSAttributedString 工具
转换 HTML String 成 NSAttributedString 对应 Key 样式设定
ZhgChgLi / ZMarkupParser
功能
使用纯 Swift 开发,透过 Regex 剖析出 HTML Tag 并经过 Tokenization,分析修正 Tag 正确性(修正没有 end 的 tag & 错位 tag),再转换成 abstract syntax tree,最终使用 Visitor Pattern 将 HTML Tag 与抽象样式对应,得到最终 NSAttributedString 结果;其中不依赖任何 Parser Lib。
支援 HTML Render (to NSAttributedString) / Stripper (剥离 HTML Tag) / Selector 功能
自动分析修正 Tag 正确性(修正没有 end 的 tag & 错位 tag)
<br>
-><br/>
<b>Bold<i>Bold+Italic</b>Italic</i>
-><b>Bold<i>Bold+Italic</i></b><i>Italic</i>
<Congratulation!>
-><Congratulation!>
(treat as String)支援客制化样式指定 e.g.
<b></b>
->weight: .semilbold & underline: 1
支援自行扩充 HTML Tag 解析 e.g. 解析
<zhgchgli></zhgchgli>
成想要的样式包含架构设计,方便对 HTML Tag 进行扩充 目前纯了支援基本的样式之外还支援 ul/ol/li 列表及 hr 分隔线渲染,未来要扩充支援其他 HTML Tag 也能快速支援
支援从
style
HTML Attribute 扩充解析样式 HTML 可以从 style 指定文字样式,同样的,此套件也能支援从style
中指定样式 e.g.<b style=”font-size: 20px”></b>
->粗体+字型 20 px
支援 iOS/macOS
支援 HTML Color Name to UIColor/NSColor
Test Coverage: 80%+
支援
<img>
图片、<ul>
项目清单、<table>
表格…等等 HTMLTag 解析比
NSAttributedString.DocumentType.html
更高的效能
效能分析
测试环境:2022/M2/24GB Memory/macOS 13.2/XCode 14.1
X 轴:HTML 字数
Y 轴:渲染所花时间(秒)
*另外 NSAttributedString.DocumentType.html
超过 54,600+ 长度字串就会闪退 (EXC_BAD_ACCESS)。
试玩
可直接下载专案打开 ZMarkupParser.xcworkspace
选择 ZMarkupParser-Demo
Target Build & Run 直接测试效果。
安装
支援 SPM/Cocoapods ,请参考 Readme 。
使用方式
样式宣告
MarkupStyle/MarkupStyleColor/MarkupStyleParagraphStyle,对应 NSAttributedString.Key 的封装。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var font:MarkupStyleFont
var paragraphStyle:MarkupStyleParagraphStyle
var foregroundColor:MarkupStyleColor? = nil
var backgroundColor:MarkupStyleColor? = nil
var ligature:NSNumber? = nil
var kern:NSNumber? = nil
var tracking:NSNumber? = nil
var strikethroughStyle:NSUnderlineStyle? = nil
var underlineStyle:NSUnderlineStyle? = nil
var strokeColor:MarkupStyleColor? = nil
var strokeWidth:NSNumber? = nil
var shadow:NSShadow? = nil
var textEffect:String? = nil
var attachment:NSTextAttachment? = nil
var link:URL? = nil
var baselineOffset:NSNumber? = nil
var underlineColor:MarkupStyleColor? = nil
var strikethroughColor:MarkupStyleColor? = nil
var obliqueness:NSNumber? = nil
var expansion:NSNumber? = nil
var writingDirection:NSNumber? = nil
var verticalGlyphForm:NSNumber? = nil
...
可依照自己想套用到 HTML Tag 上对应的样式自行宣告:
1
let myStyle = MarkupStyle(font: MarkupStyleFont(size: 13), backgroundColor: MarkupStyleColor(name: .aquamarine))
HTML Tag
宣告要渲染的 HTML Tag 与对应的 Markup Style,目前预定义的 HTML Tag Name 如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
A_HTMLTagName(), // <a></a>
B_HTMLTagName(), // <b></b>
BR_HTMLTagName(), // <br></br>
DIV_HTMLTagName(), // <div></div>
HR_HTMLTagName(), // <hr></hr>
I_HTMLTagName(), // <i></i>
LI_HTMLTagName(), // <li></li>
OL_HTMLTagName(), // <ol></ol>
P_HTMLTagName(), // <p></p>
SPAN_HTMLTagName(), // <span></span>
STRONG_HTMLTagName(), // <strong></strong>
U_HTMLTagName(), // <u></u>
UL_HTMLTagName(), // <ul></ul>
DEL_HTMLTagName(), // <del></del>
IMG_HTMLTagName(handler: ZNSTextAttachmentHandler), // <img> and image downloader
TR_HTMLTagName(), // <tr>
TD_HTMLTagName(), // <td>
TH_HTMLTagName(), // <th>
...and more
...
这样解析 <a>
Tag 时就会套用到指定的 MarkupStyle。
扩充 HTMLTagName:
1
let zhgchgli = ExtendTagName("zhgchgli")
HTML Style Attribute
如同前述,HTML 支援从 Style Attribute 指定样式,这边也抽象出来可指定支援的样式跟扩充,目前预定义的 HTML Style Attribute 如下:
1
2
3
4
5
6
7
ColorHTMLTagStyleAttribute(), // color
BackgroundColorHTMLTagStyleAttribute(), // background-color
FontSizeHTMLTagStyleAttribute(), // font-size
FontWeightHTMLTagStyleAttribute(), // font-weight
LineHeightHTMLTagStyleAttribute(), // line-height
WordSpacingHTMLTagStyleAttribute(), // word-spacing
...
扩充 Style Attribute:
1
2
3
4
5
6
7
8
9
ExtendHTMLTagStyleAttribute(styleName: "text-decoration", render: { value in
var newStyle = MarkupStyle()
if value == "underline" {
newStyle.underline = NSUnderlineStyle.single
} else {
// ...
}
return newStyle
})
使用
1
2
3
import ZMarkupParser
let parser = ZHTMLParserBuilder.initWithDefault().set(rootStyle: MarkupStyle(font: MarkupStyleFont(size: 13)).build()
initWithDefault
会自动加入预先定义的 HTML Tag Name & 预设对应的 MarkupStyle 还有预先定义的 Style Attribute。
set(rootStyle:)
可指定整个字串的预设样式,也可不指定。
客制化
1
2
let parser = ZHTMLParserBuilder.initWithDefault().add(ExtendTagName("zhgchgli"), withCustomStyle: MarkupStyle(backgroundColor: MarkupStyleColor(name: .aquamarine))).build() // will use markupstyle you specify to render extend html tag <zhgchgli></zhgchgli>
let parser = ZHTMLParserBuilder.initWithDefault().add(B_HTMLTagName(), withCustomStyle: MarkupStyle(font: MarkupStyleFont(size: 18, weight: .style(.semibold)))).build() // will use markupstyle you specify to render <b></b> instead of default bold markup style
HTML Render
1
2
3
4
5
6
let attributedString = parser.render(htmlString) // NSAttributedString
// work with UITextView
textView.setHtmlString(htmlString)
// work with UILabel
label.setHtmlString(htmlString)
HTML Stripper
1
parser.stripper(htmlString)
Selector HTML String
1
2
3
4
5
6
7
let selector = parser.selector(htmlString) // HTMLSelector e.g. input: <a><b>Test</b>Link</a>
selector.first("a")?.first("b").attributedString // will return Test
selector.filter("a").attributedString // will return Test Link
// render from selector result
let selector = parser.selector(htmlString) // HTMLSelector e.g. input: <a><b>Test</b>Link</a>
parser.render(selector.first("a")?.first("b"))
Async
另外如果要渲染长字串,可改用 async 方法,防止卡 UI。
1
2
3
parser.render(String) { _ in }...
parser.stripper(String) { _ in }...
parser.selector(String) { _ in }...
Know-how
UITextView 中的超连结样式是看 linkTextAttributes,所以会出现 NSAttributedString.key 明明有设定但却没出现效果的情况。
UILabel 不支援指定 URL 样式,所以会出现 NSAttributedString.key 明明有设定但却没出现效果的情况。
如果要渲染复杂的 HTML,还是需要使用 WKWebView (包含 JS/表格. .渲染)。
技术原理及开发故事:「 手工打造 HTML 解析器的那些事 」
欢迎贡献及提出 Issue 将尽快修正
有任何问题及指教欢迎 与我联络 。
本文首次发表于 Medium (点击查看原始版本),由 ZMediumToMarkdown 提供自动转换与同步技术。