Post

ZMarkupParser HTML String 轉換 NSAttributedString 工具

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

ZMarkupParser HTML String 轉換 NSAttributedString 工具

ℹ️ℹ️ℹ️ Click here to view the English version of this article, translated by OpenAI.


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
...

這樣解析 <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 ➡️ 前往查看

ZMediumToMarkdownMedium-to-jekyll-starter 提供自動轉換與同步技術。

Improve this page on Github.

Buy me a beer

681 Total Views
Last Statistics Date: 2025-03-25 | 650 Views on Medium.
This post is licensed under CC BY 4.0 by the author.