Skip to content

Instantly share code, notes, and snippets.

@fxm90
Last active January 27, 2026 16:21
Show Gist options
  • Select an option

  • Save fxm90/abd949e4258050f2f3cd80118024e5bd to your computer and use it in GitHub Desktop.

Select an option

Save fxm90/abd949e4258050f2f3cd80118024e5bd to your computer and use it in GitHub Desktop.
Extension that converts Strings with basic HTML tags to SwiftUI's Text (Supports SwiftUI 3.0 / iOS 15.0).
//
// SwiftUI+HTML.swift
//
// Created by Felix Mau on 28.05.21.
// Copyright © 2021 Felix Mau. All rights reserved.
//
import SwiftUI
/// A lightweight utility that converts a limited subset of HTML tags
/// into their Markdown equivalents.
///
/// - SeeAlso: ``HTMLToMarkdownConverter.Tag`` for the list of supported tags.
@available(iOS 15.0, *)
enum HTMLToMarkdownConverter {
// MARK: - Public Methods
/// Converts supported HTML tags in the given string into their
/// corresponding Markdown syntax.
///
/// - Parameter html: A string containing HTML markup.
///
/// - Returns: A Markdown-formatted string representing the supported
/// HTML tags found in the input.
///
/// - SeeAlso: ``HTMLToMarkdownConverter.Tag`` for the list of supported tags.
static func convert(_ html: String) -> String {
// Convert "basic" HTML tags that don't use an attribute.
let markdown = Tag.allCases.reduce(html) { result, tag in
result
.replacingOccurrences(of: tag.openingHtmlTag, with: tag.markdownDelimiter)
.replacingOccurrences(of: tag.closingHtmlTag, with: tag.markdownDelimiter)
}
// Anchor tags (`<a>`) use an attribute and therefore needs to be handled differently.
return convertAnchorTagsToMarkdown(markdown)
}
// MARK: - Private Methods
/// Converts HTML anchor (`<a>`) tags into their Markdown representation.
///
/// Only the following syntax is supported:
/// `<a href="URL">TEXT</a>` → `[TEXT](URL)`
///
/// Anchor tags with additional attributes (e.g. `target`, `rel`, nested elements, or multiline content)
/// are **not supported** and may produce incorrect results.
///
/// - Parameter html: A string potentially containing HTML anchor tags.
///
/// - Returns: A string where supported HTML links are replaced with Markdown links.
private static func convertAnchorTagsToMarkdown(_ html: String) -> String {
html.replacingOccurrences(
of: "<a href=\"(.+)\">(.+)</a>",
with: "[$2]($1)",
options: .regularExpression,
range: nil,
)
}
}
// MARK: - Supporting Types
extension HTMLToMarkdownConverter {
/// A supported inline HTML tag that can be converted to Markdown.
///
/// Each case represents an HTML tag whose opening and closing tags
/// are replaced with a corresponding Markdown delimiter.
enum Tag: String, CaseIterable {
case strong
case em
case s
case code
/// The opening HTML tag (e.g. `<strong>`).
var openingHtmlTag: String {
"<\(rawValue)>"
}
/// The closing HTML tag (e.g. `</strong>`).
var closingHtmlTag: String {
"</\(rawValue)>"
}
/// The Markdown delimiter corresponding to the HTML tag.
var markdownDelimiter: String {
switch self {
case .strong:
"**"
case .em:
"*"
case .s:
"~~"
case .code:
"`"
}
}
}
}
@available(iOS 15.0, *)
extension Text {
// MARK: - Initializer
/// Creates a `Text` view by rendering a string containing supported HTML tags.
///
/// The HTML is first converted to Markdown using ``HTMLToMarkdownConverter``,
/// then parsed into an `AttributedString`.
///
/// - Parameter html: A string containing supported HTML markup.
///
/// - Note: If Markdown parsing fails, the initializer falls back
/// to rendering the raw Markdown string without formatting.
///
/// - SeeAlso: ``HTMLToMarkdownConverter.Tag`` for the list of supported tags.
init(html: String) {
let markdown = HTMLToMarkdownConverter.convert(html)
do {
let markdownAsAttributedString = try AttributedString(markdown: markdown)
self = .init(markdownAsAttributedString)
} catch {
print("⚠️ – Couldn't parse markdown:", error)
// Render the raw Markdown string without formatting as fallback.
self = .init(markdown)
}
}
}
@fxm90
Copy link
Author

fxm90 commented Jan 27, 2026

Related test when using Swift Testing

import Testing

@Suite
struct HTMLToMarkdownConverterTests {

  @Test
  func convert_shouldIgnoreStringWithoutHTMLTags() {
    // Given
    let html = "Lorem ipsum dolor sit amet."

    // When
    let markdown = HTMLToMarkdownConverter.convert(html)

    // Then
    #expect(html == markdown)
  }

  @Test
  func convert_shouldIgnoreUnsupportedHTMLTags() {
    // Given
    let html = "<p>Lorem ipsum dolor sit amet.</p>"

    // When
    let markdown = HTMLToMarkdownConverter.convert(html)

    // Then
    #expect(html == markdown)
  }

  @Test(arguments: [
    "<strong>Lorem</strong> ipsum <strong>dolor</strong> sit <strong>amet</strong>": "**Lorem** ipsum **dolor** sit **amet**",
    "<em>Lorem</em> ipsum <em>dolor</em> sit <em>amet</em>": "*Lorem* ipsum *dolor* sit *amet*",
    "<s>Lorem</s> ipsum <s>dolor</s> sit <s>amet</s>": "~~Lorem~~ ipsum ~~dolor~~ sit ~~amet~~",
    "<code>Lorem</code> ipsum <code>dolor</code> sit <code>amet</code>": "`Lorem` ipsum `dolor` sit `amet`",
    "Visit <a href=\"https://apple.com\">Apple</a>": "Visit [Apple](https://apple.com)",
  ])
  func convert_shouldReplaceSupportedHTMLTags(html: String, expectedMarkdown: String) {
    // When
    let markdown = HTMLToMarkdownConverter.convert(html)

    // Then
    #expect(markdown == expectedMarkdown)
  }

  @Test
  func convert_shouldHandleMultipleTags() {
    // Given
    let html = "Visit <a href=\"https://apple.com\">Apple</a>. <strong>Lorem</strong> <em>ipsum</em>"

    // When
    let markdown = HTMLToMarkdownConverter.convert(html)

    // Then
    #expect(markdown == "Visit [Apple](https://apple.com). **Lorem** *ipsum*")
  }

  @Test
  func convert_shouldHandleNestedTags() {
    // Given
    let html = "Visit <strong><em><a href=\"https://apple.com\">Apple</a></em></strong>"

    // When
    let markdown = HTMLToMarkdownConverter.convert(html)

    // Then
    #expect(markdown == "Visit ***[Apple](https://apple.com)***")
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment