Skip to content
CreatureSurvivePublic

About

Fast, thread-safe HTML to AttributedString for SwiftUI, UIKit and AppKit, without WebKit. Works on tvOS and watchOS.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

HTMLText

CI Swift 6.1+ Platforms Swift Package Manager License: MIT

Fast, thread-safe HTML → AttributedString conversion for SwiftUI, UIKit and AppKit. It uses no WebKit, so it also works on tvOS and watchOS.

import HTMLText

Text(AttributedString(html: item.overview))

Why

Media servers, forums, feeds and CMS APIs return HTML fragments: Jellyfin and Plex overviews, RSS descriptions, comments. Apple's only built-in converter is NSAttributedString(data:options:[.documentType: .html]). It:

  • runs a hidden WebKit instance, and is slow (milliseconds per call) and memory-hungry
  • must run on the main thread and can deadlock or crash when it doesn't
  • spins the run loop re-entrantly, which breaks SwiftUI updates and scrolling
  • produces Times New Roman text with web styling you then have to strip out
  • returns NSAttributedString, not the AttributedString SwiftUI wants

HTMLText parses HTML itself. It runs on any thread or actor and returns a semantic AttributedString that SwiftUI's Text renders in your app's fonts.

Features

  • Lenient parser that repairs malformed markup the way browsers do: implied closes (<p>, <li>, table cells), mis-nested tags, stray end tags, unquoted attributes, bare < and &. Scripts, styles, comments and <head> are dropped.
  • Complete entity decoding: the full WHATWG table (2,125 named references), numeric references with Windows-1252 remapping, and legacy semicolon-less forms, all per spec.
  • Correct text layout: HTML whitespace collapsing, paragraph and line breaks, <pre>, nested ordered/unordered lists with aligned markers (start, reversed, value), blockquotes, definition lists, tables, <hr>, <q>, and image alt text.
  • Semantic output: bold, italic, code and strikethrough map to inlinePresentationIntent, which SwiftUI Text renders natively. Links become tappable link attributes, resolved against a base URL, with schemes filtered by an allow list so javascript: never becomes a link.
  • Styling hooks: headings, underline, superscript/subscript, <mark> and blockquotes get SwiftUI attributes. Provide your own HTMLStyleSheet to change any of it.
  • UIKit/AppKit: NSAttributedString(html:baseFont:) derives bold, italic, monospaced and heading fonts from your base font.
  • Plain text: HTML.plainText(from:) uses the same layout rules, for previews, search indexes and accessibility labels.
  • Safe on hostile input: rendering is iterative with a nesting cap, so deeply nested or garbage markup can't overflow the stack.

Installation

Add HTMLText to your Package.swift:

dependencies: [
    .package(url: "https://github.com/CreatureSurvive/HTMLText.git", from: "1.0.0"),
],
targets: [
    .target(name: "MyApp", dependencies: ["HTMLText"]),
]

Or in Xcode, choose File › Add Package Dependencies… and enter https://github.com/CreatureSurvive/HTMLText.

Requirements

Platform Minimum
iOS 16.0
macOS 13.0
tvOS 16.0
watchOS 9.0
visionOS 1.0

Swift 6.1 (Xcode 16.4) or later, in Swift 6 language mode. No third-party dependencies.

Usage

SwiftUI

Text(AttributedString(html: "<p>A <b>bold</b> and <i>italic</i> <a href=\"/about\">link</a>.</p>",
                      options: HTMLRenderOptions(baseURL: serverURL)))

Convert off the main thread for large content:

.task(id: html) {
    attributed = await Task.detached { AttributedString(html: html) }.value
}

Plain text

let preview = HTML.plainText(from: overview)       // "Line one.\n\nLine two."
if HTML.containsMarkup(text) { ... }

UIKit / AppKit

label.attributedText = NSAttributedString(html: html, baseFont: .preferredFont(forTextStyle: .body), textColor: .label)

Options

var options = HTMLRenderOptions(baseURL: URL(string: "https://example.com"))
options.paragraphSpacing = 1                 // no blank line between paragraphs
options.bulletMarkers = ["–"]
options.images = .omit
options.allowedLinkSchemes.insert("myapp")

Custom styling

let styleSheet = HTMLStyleSheet { style in
    var container = HTMLStyleSheet.semantic.attributes(style)
    if style.headingLevel != nil { container.swiftUI.foregroundColor = .accentColor }
    if style.code { container.swiftUI.font = .system(.body, design: .monospaced) }
    return container
}
Text(AttributedString(html: html, styleSheet: styleSheet))

Lower-level access

let tree = HTML.parse(html)                       // HTMLElement tree
let runs = HTMLLayout().runs(for: tree)           // [HTMLRun] — text + semantic style
HTMLEntityDecoder.decode("Tom &amp; Jerry")        // "Tom & Jerry"
htmlEscape("<b>")                                  // "&lt;b&gt;"

Performance

A typical 1–5 KB overview converts in well under a millisecond. A 1.2 MB document with 60,000 styled runs converts in about 0.9 s in a release build. Conversion is thread-safe, so batches can be parallelized.

Scope

HTMLText targets content HTML, not web pages. CSS is ignored apart from semantic tags, and there is no layout engine or JavaScript. Use WKWebView for full pages.

Changelog

See CHANGELOG.md. Releases follow Semantic Versioning.

Contributing

Issues and pull requests are welcome. Please run swift test before opening a pull request, and add tests for new behavior.

License

Available under the MIT license. See LICENSE for details.

The entity table is generated from the WHATWG HTML Standard (CC BY 4.0).

About

Fast, thread-safe HTML to AttributedString for SwiftUI, UIKit and AppKit, without WebKit. Works on tvOS and watchOS.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages