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 將盡快修正
有任何問題及指教歡迎 與我聯絡 。
===
View the English version of this article here.
本文首次發表於 Medium ➡️ 前往查看