Home ZMarkupParser HTML String 轉換 NSAttributedString 工具
Post
Cancel

ZMarkupParser HTML String 轉換 NSAttributedString 工具

ZMarkupParser HTML String 轉換 NSAttributedString 工具

轉換 HTML String 成 NSAttributedString 對應 Key 樣式設定

ZhgChgLi / ZMarkupParser

[ZhgChgLi](https://github.com/ZhgChgLi){:target="_blank"} [ZMarkupParser](https://github.com/ZhgChgLi/ZMarkupParser){:target="_blank"}

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 更高的效能

效能分析

[Performance Benchmark](https://quickchart.io/chart-maker/view/zm-73887470-e667-4ca3-8df0-fe3563832b0b){:target="_blank"}

Performance Benchmark

  • 測試環境: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
...

這樣解析 &lt;a&gt; 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 ➡️ 前往查看


This post is licensed under CC BY 4.0 by the author.

Google 搜尋出現與本人李仲澄無關之負面新聞聲明

The Chronicles of Crafting an HTML Parser from Scratch