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))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 theAttributedStringSwiftUI 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.
- 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 SwiftUITextrenders natively. Links become tappablelinkattributes, resolved against a base URL, with schemes filtered by an allow list sojavascript:never becomes a link. - Styling hooks: headings, underline, superscript/subscript,
<mark>and blockquotes get SwiftUI attributes. Provide your ownHTMLStyleSheetto 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.
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.
| 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.
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
}let preview = HTML.plainText(from: overview) // "Line one.\n\nLine two."
if HTML.containsMarkup(text) { ... }label.attributedText = NSAttributedString(html: html, baseFont: .preferredFont(forTextStyle: .body), textColor: .label)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")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))let tree = HTML.parse(html) // HTMLElement tree
let runs = HTMLLayout().runs(for: tree) // [HTMLRun] — text + semantic style
HTMLEntityDecoder.decode("Tom & Jerry") // "Tom & Jerry"
htmlEscape("<b>") // "<b>"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.
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.
See CHANGELOG.md. Releases follow Semantic Versioning.
Issues and pull requests are welcome. Please run swift test before opening a pull request, and
add tests for new behavior.
Available under the MIT license. See LICENSE for details.
The entity table is generated from the WHATWG HTML Standard (CC BY 4.0).