Post

ZMarkupParser HTML String 转换 NSAttributedString 工具

转换 HTML String 成 NSAttributedString 对应 Key 样式设定

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