ZhgChg.Li

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

ZMarkupParser HTML String to NSAttributedString Converter Tool

Independent writing, free to read — please support these ads

 

Advertise here →

Convert HTML String to NSAttributedString Corresponding Key Style Settings

ZhgChgLi / ZMarkupParser

ZhgChgLi / ZMarkupParser

ZhgChgLi / ZMarkupParser

Features

  • Developed purely in Swift, it parses HTML tags using Regex and performs tokenization, analyzes and corrects tag accuracy (fixing tags without ends & 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; it does not rely on any parser libraries.

  • Supports HTML Render (to NSAttributedString) / Stripper (Remove HTML Tags) / Selector Features

  • Automatically Analyze and Correct 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 Extension of HTML Tag Parsing
    e.g. Parse <zhgchgli></zhgchgli> into the desired style

  • Includes architecture design for easy HTML tag extension
    Currently, besides supporting basic styles, it also supports rendering ul/ol/li lists and hr separators. Future extensions for other HTML tags can be quickly supported.

  • Support for Extended Style Parsing from style HTML Attribute
    HTML can specify text styles through the style attribute. Similarly, this package supports specifying styles from the style attribute.
    e.g. <b style=”font-size: 20px”></b> -> Bold + font size 20 px

  • Supports iOS/macOS

  • Support HTML Color Name to UIColor/NSColor

  • Test Coverage: 80%+

  • Supports parsing of HTML tags such as <img> images, <ul> lists, <table> tables, and more.

  • Higher Performance than NSAttributedString.DocumentType.html

Performance Analysis

Performance Benchmark

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 & run to test the functionality.

Installation

Supports SPM/Cocoapods. Please refer to Readme.

Usage

Style Declaration

MarkupStyle/MarkupStyleColor/MarkupStyleParagraphStyle are wrappers corresponding to NSAttributedString.Key.

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 your own styles to apply to the corresponding HTML tags as you wish:

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

HTML Tag

Declare the HTML tags to be rendered and their corresponding Markup Styles. The currently predefined HTML tag names are as follows:

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
...
// HTMLTag
HTMLTag(tagName: A_HTMLTagName(), customStyle: myStyle)

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

Extending HTMLTagName:

let zhgchgli = ExtendTagName("zhgchgli")

HTML Style Attribute

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

ColorHTMLTagStyleAttribute(), // color
BackgroundColorHTMLTagStyleAttribute(), // background-color
FontSizeHTMLTagStyleAttribute(), // font-size
FontWeightHTMLTagStyleAttribute(), // font-weight
LineHeightHTMLTagStyleAttribute(), // line-height
WordSpacingHTMLTagStyleAttribute(), // word-spacing
...

Extend Style Attribute:

ExtendHTMLTagStyleAttribute(styleName: "text-decoration", render: { value in
  var newStyle = MarkupStyle()
  if value == "underline" {
    newStyle.underline = NSUnderlineStyle.single
  } else {
    // ...  
  }
  return newStyle
})

Usage

Independent writing, free to read — please support these ads

 

Advertise here →
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 specify the default style for the entire string, or it can be left unspecified.

Customization

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

let attributedString = parser.render(htmlString) // NSAttributedString

// work with UITextView
textView.setHtmlString(htmlString)
// work with UILabel
label.setHtmlString(htmlString)

HTML Stripper

parser.stripper(htmlString)

Selector HTML String

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

For rendering long strings, you can use the async method to prevent UI blocking.

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

Know-how

  • The hyperlink style in UITextView depends on linkTextAttributes, so even if NSAttributedString.key is set, the effect may not appear.

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

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

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

Contributions and Issues Are Welcome and Will Be Addressed Promptly

Independent writing, free to read — please support these ads

 

Advertise here →

Improve this page
Edit on GitHub
Also published on Medium
Read the original
Share this essay
Copy link · share to socials
ZhgChgLi
Author

ZhgChgLi

An iOS, web, and automation developer from Taiwan 🇹🇼 who also loves sharing, traveling, and writing.

Comments