Post

ZMarkupParser|Convert HTML String to NSAttributedString with Custom Style Keys

Developers facing challenges in converting HTML strings to NSAttributedString can utilize ZMarkupParser to apply precise style key mappings, ensuring accurate and customizable text rendering in iOS applications.

ZMarkupParser|Convert HTML String to NSAttributedString with Custom Style Keys

点击这里查看本文章简体中文版本。

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

This post was translated with AI assistance — let me know if anything sounds off!


ZMarkupParser HTML String to NSAttributedString Converter Tool

Convert HTML String to NSAttributedString with Corresponding Key Style Settings

ZhgChgLi / ZMarkupParser

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

ZhgChgLi / ZMarkupParser

Features

  • Developed purely in Swift, it uses Regex to parse HTML tags and tokenize them. It analyzes and corrects tag accuracy (fixing tags without an end and misaligned tags), then converts them into an abstract syntax tree. Finally, it uses the Visitor Pattern to map HTML tags to abstract styles, producing the final NSAttributedString result. No parser libraries are used.

  • Supports HTML Render (to NSAttributedString) / Stripper (removes HTML tags) / Selector functions

  • Automatic Analysis and Correction of Tag Accuracy (Fix Missing End Tags & Misplaced Tags)
    <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)

  • Support custom style specification
    e.g. <b></b> -> weight: .semibold & underline: 1

  • Support custom HTML tag parsing
    e.g. Parse <zhgchgli></zhgchgli> into the desired style

  • Includes architecture design to facilitate expansion of HTML Tag support
    Currently supports basic styles as well as ul/ol/li lists and hr divider rendering. Future expansions will allow quick support for other HTML Tags.

  • Support extending style parsing from the style HTML Attribute
    HTML allows text styles to be specified via the style attribute. Similarly, this package also supports styles defined in the style attribute.
    e.g. <b style=”font-size: 20px”></b> -> bold + font size 20 px

  • Support iOS/macOS

  • Support HTML Color Name to UIColor/NSColor

  • Test Coverage: 80%+

  • Supports parsing of <img> images, <ul> item lists, <table> tables, and other HTML tags.

  • Higher Performance than NSAttributedString.DocumentType.html

Performance Analysis

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

Performance Benchmark

  • Test Environment: 2022/M2/24GB Memory/macOS 13.2/XCode 14.1

  • X Axis: Number of HTML Characters

  • Y-axis: Rendering Time (seconds)

*Additionally, NSAttributedString.DocumentType.html will crash (EXC_BAD_ACCESS) with strings longer than 54,600+ characters.

Try it out

You can directly download the project, open ZMarkupParser.xcworkspace, select the ZMarkupParser-Demo target, then build and run to test the functionality.

Installation

Supports SPM/Cocoapods, please refer to Readme.

How to Use

Style Declaration

MarkupStyle/MarkupStyleColor/MarkupStyleParagraphStyle are wrappers for 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
...

You can declare styles according to the HTML tags you want to apply them to:

1
let myStyle = MarkupStyle(font: MarkupStyleFont(size: 13), backgroundColor: MarkupStyleColor(name: .aquamarine))

HTML Tag

Declare the HTML tags to render and their corresponding markup styles. The currently predefined HTML tag names are as follows:

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

This way, when parsing the <a> tag, the specified MarkupStyle will be applied.

Extend HTMLTagName:

1
let zhgchgli = ExtendTagName("zhgchgli")

HTML Style Attribute

As mentioned earlier, HTML supports specifying styles via the Style Attribute. Here, we also abstract the supported styles and extensions. The currently predefined HTML Style Attributes are as follows:

1
2
3
4
5
6
7
ColorHTMLTagStyleAttribute(), // color
BackgroundColorHTMLTagStyleAttribute(), // background-color
FontSizeHTMLTagStyleAttribute(), // font-size
FontWeightHTMLTagStyleAttribute(), // font-weight
LineHeightHTMLTagStyleAttribute(), // line-height
WordSpacingHTMLTagStyleAttribute(), // word-spacing
...

Extend 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
})

Usage

1
2
3
import ZMarkupParser

let parser = ZHTMLParserBuilder.initWithDefault().set(rootStyle: MarkupStyle(font: MarkupStyleFont(size: 13))).build()

initWithDefault automatically adds predefined HTML Tag Names, default corresponding MarkupStyles, and predefined Style Attributes.

set(rootStyle:) can set the default style for the entire string, or it can be left unspecified.

Customization

1
2
let parser = ZHTMLParserBuilder.initWithDefault().add(ExtendTagName("zhgchgli"), withCustomStyle: MarkupStyle(backgroundColor: MarkupStyleColor(name: .aquamarine))).build() // will use the markup style you specify to render the extended HTML tag <zhgchgli></zhgchgli>
let parser = ZHTMLParserBuilder.initWithDefault().add(B_HTMLTagName(), withCustomStyle: MarkupStyle(font: MarkupStyleFont(size: 18, weight: .style(.semibold)))).build() // will use the markup style you specify to render <b></b> instead of the 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

Additionally, if rendering long strings, you can use async methods to prevent UI blocking.

1
2
3
parser.render(String) { _ in }...
parser.stripper(String) { _ in }...
parser.selector(String) { _ in }...

Know-how

  • The hyperlink style in UITextView depends on linkTextAttributes, so there may be cases where NSAttributedString.Key is set but has no visible effect.

  • UILabel does not support specifying URL styles, so NSAttributedString.key may be set but have no visible effect.

  • If you need to render complex HTML, you still need to use WKWebView (including JS/tables rendering).

Technical Principles and Development Story: “The Story of Manually Building an HTML Parser

Welcome Contributions and Issue Reporting for Prompt Fixes

If you have any questions or feedback, feel free to contact me.


Buy me a beer

This post was originally published on Medium (View original post), and automatically converted and synced by ZMediumToMarkdown.

Improve this page on Github.

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