ZMarkupParser

ZMarkupParser helps you to convert HTML String to NSAttributedString with customized style and tag through pure-Swift.

Features

  • Parse HTML String through Rexgex with pure-Swift.
  • Autocorrect invalid HTML string, including mixed or isolated tag. (e.g. <a>Link<b>LinkBold</a>Bold</b><br> -> <a>Link<b>LinkBold</b></a><b>Bold</b><br/>)
  • Painless extended HTML Tag Parser and customized HTML Tag Style you wish.
  • Support HTML Render/HTML Stripper/HTML Selector
  • Support <ul><ol> list view and <hr> horizontal line…etc
  • Support parse & set style from html tag style="color:red" attributes.
  • Support parse HTML Color name to UIColor/NSColor.
  • Higher performance than NSAttributedString.DocumentType.html

Try it!

downloaded repo and open ZMarkupParser.xcworkspace, select target to ZMarkupParser-Demo run it! have fun!

Performance Benchmark

chart (1)

(2022/M2/24GB Memory/macOS 13.2/XCode 14.1)

NSAttributedString.DocumentType.html will crashed when render more than 54,600+ length of string.

  • x: html string length
  • y: time elapsed(second)

Installation

Swift Package Manager

  • File > Swift Packages > Add Package Dependency
  • Add https://github.com/ZhgChgLi/ZMarkupParser.git
  • Select “Up to Next Major” with “1.1.6”

or

...
dependencies: [
  .package(url: "https://github.com/ZhgChgLi/ZMarkupParser.git", from: "1.1.6"),
]
...
.target(
    ...
    dependencies: [
        "ZMarkupParser",
    ],
    ...
)

CocoaPods

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '12.0'
use_frameworks!

target 'MyApp' do
  pod 'ZMarkupParser', '~> 1.1.6'
end

How it works? (explain with Pseudocode)

  1. Input html string: <a>Link<b>LinkBold</a>Bold</b>
  2. Convert string to array of tag element through Regex: [{tagStrat: a}, {string: Link}, {tagStart: b}, {string: LinkBold}, {tagClose: a}, {string: Bold}, {tagClose: b}]
  3. Traversal tag element array to autocorrent mixed tag and find isolated tag: [{tagStrat: a}, {string: Link}, {tagStart: b}, {string: LinkBold}, {tagClose: b}, {tagClose: a}, {tagStart: b}, {string: Bold}, {tagClose: b}]
  4. Convert tag element array to abstract syntax tree:

//                     RootMarkup
//                  /               \
//                 A                 B
//          /             \          |
//    String("Link")       B    String("Bold")  
//                         |
//                  String("LinkBold")
//                                
  1. Mapping tag to abstract Markup/MarkupStyle
  2. Use Visitor Pattern to visit every tree leaf Markup/MarkupStyle and combine it to NSAttributedString through recursion.
  3. Result:

Link{
    NSColor = "UIExtendedSRGBColorSpace 0 0.478431 1 1";
    NSFont = "<UICTFont: 0x145d17600> font-family: \".SFUI-Regular\"; font-weight: normal; font-style: normal; font-size: 13.00pt";
    NSUnderline = 1;
}LinkBold{
    NSColor = "UIExtendedSRGBColorSpace 0 0.478431 1 1";
    NSFont = "<UICTFont: 0x145d18710> font-family: \".SFUI-Semibold\"; font-weight: bold; font-style: normal; font-size: 18.00pt";
    NSUnderline = 1;
}Bold{
    NSFont = "<UICTFont: 0x145d18710> font-family: \".SFUI-Semibold\"; font-weight: bold; font-style: normal; font-size: 18.00pt";
}

Example

ZMarkupParser Exmple

Introduction

HTMLTagName

Abstract of html tag, there is some pre-defined html tag name down below.

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

If there is HTML tag not be defined or customized tag, you could use ExtendTagName("tagName") to wrapped it.

MarkupStyle/MarkupStyleColor/MarkupStyleParagraphStyle

Wrapper of NSAttributedStrin.Key because of inheritable puerpose.

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 could init or define MarkupStyle you want.

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

HTMLTagStyleAttribute

Abstract of html style attributes, there is some pre-defined attributes down below.

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

If there is Style attribute not be defined, you could use ExtendHTMLTagStyleAttribute("styleAttributeName", MarkupStyle) to wrapped it.

Usage

import ZMarkupParser

Builder Pattern to Build Parser

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

initWithDefault() will add all pre-defined HTMLTagName/HTMLTagStyleAttribute and use Tag’s default MarkupStyle to render.

set(rootStyle:MarkupStyle) specify default root style to render, will apply to whole attributed string.

build() call build() at the end, to generate parser object.

Customized Tag Style/Extend Tag Name

let parser = ZHTMLParserBuilder.initWithDefault().add(B_HTMLTagName(), withCustomStyle: MarkupStyle(font: MarkupStyleFont(size: 18, weight: .style(.semibold)))) // will use markupstyle you specify to render <b></b> instead of default bold markup style
let parser = ZHTMLParserBuilder.initWithDefault().add(ExtendTagName("zhgchgli"), withCustomStyle: MarkupStyle(backgroundColor: MarkupStyleColor(name: .aquamarine))) // will use markupstyle you specify to render extend html tag <zhgchgli></zhgchgli>

Parse HTML String

parser.render(htmlString) // NSAttributedString

// work with UITextView
textView.setHtmlString(htmlString)

// work with UILabel
label.setHtmlString(htmlString)

Stripper HTML String

parser.stripper(htmlString) // NSAttributedString

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

Selector+Render HTML String

let selector = parser.selector(htmlString) // HTMLSelector e.g. input: <a><b>Test</b>Link</a>
parser.render(selector.first("a")?.first("b"))

With Async

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

If you want to render huge html string, please use async instead.

Things to know

  • Unsupport Parse <img> to NSTextAttacment currently, due to need async task & native textview with NSTextAttacment didn’t impletation reuse, insert image throught NSTextAttacment to TextView will lead to Out of memory.
  • Need to set linkTextAttributes for UITextView to change .link style
  • UILabel is not allow to change .link text color style throught NSAttributedString.key.foregroundColor
  • Due to limitation, colud only use this parser render in partial html, do not use in redner whole huge html (please use webview.loadhtml instead.)

Made In Taiwan ??????

Buy Me A Coffe

If this is helpful, please help to star the repo or recommend it to your friends.

Please feel free to open an Issue or submit a fix/contribution via Pull Request. ?

GitHub

View Github