Post

ZMarkupParser|Swift HTML String 转 NSAttributedString 工具,支援客制样式与高效能渲染

针对 iOS/macOS 开发者,提供纯 Swift 实作的 HTML String 转 NSAttributedString 工具,支援自动修正错误标签、客制样式设定与多种 HTML Tag,提升渲染效能并避免长字串闪退,助你打造流畅又稳定的文字显示效果。

ZMarkupParser|Swift HTML String 转 NSAttributedString 工具,支援客制样式与高效能渲染

Click here to view the English version of this article.

點擊這裡查看本文章正體中文版本。

基于 SEO 考量,本文标题与描述经 AI 调整,原始版本请参考内文。


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 将尽快修正

有任何问题及指教欢迎 与我联络


Buy me a beer

本文首次发表于 Medium (点击查看原始版本),由 ZMediumToMarkdown 提供自动转换与同步技术。

Improve this page on Github.

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