Skip to content

Instantly share code, notes, and snippets.

@fxm90
Last active March 19, 2026 16:26
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
@available(iOS 15.0, *)
enum HTMLToMarkdownConverter {
// MARK: - Public methods
/// Converts the HTML-tags in the given string to their corresponding markdown tags.
///
/// - SeeAlso: See type `HTMLToMarkdownConverter.Tags` for a list of supported HTML-tags.
static func convert(_ htmlAsString: String) -> String {
// Convert "basic" HTML-tags that don't use an attribute.
let markdownAsString = Tags.allCases.reduce(htmlAsString) { result, textFormattingTag in
result
.replacingOccurrences(of: textFormattingTag.openingHtmlTag, with: textFormattingTag.markdownTag)
.replacingOccurrences(of: textFormattingTag.closingHtmlTag, with: textFormattingTag.markdownTag)
}
// Hyperlinks use an attribute and therefore need to be handled differently.
return convertHtmlLinksToMarkdown(markdownAsString)
}
// MARK: - Private methods
/// Converts hyperlinks in HTML-format to their corresponding markdown representations.
///
/// - Note: Currently we only support a basic HTML syntax without any attributed other than `href`.
/// E.g. `<a href="URL">TEXT</a>` will be converted to `[TEXT](URL)`
///
/// - Parameter htmlAsString: The string containing hyperlinks in HTML-format.
///
/// - Returns: A string with hyperlinks converted to their corresponding markdown representations.
private static func convertHtmlLinksToMarkdown(_ htmlAsString: String) -> String {
htmlAsString.replacingOccurrences(of: "<a href=\"(.+)\">(.+)</a>",
with: "[$2]($1)",
options: .regularExpression,
range: nil)
}
}
extension HTMLToMarkdownConverter {
/// The supported tags inside a string we can format.
enum Tags: String, CaseIterable {
case strong
case em
case s
case code
// Hyperlinks need to be handled differently, as they not only have simple opening and closing tag, but also use the attribute `href`.
// See private method `Text.convertHtmlLinksToMarkdown(:)` for further details.
// case a
// MARK: - Public properties
var openingHtmlTag: String {
"<\(rawValue)>"
}
var closingHtmlTag: String {
"</\(rawValue)>"
}
var markdownTag: String {
switch self {
case .strong:
return "**"
case .em:
return "*"
case .s:
return "~~"
case .code:
return "`"
}
}
}
}
@available(iOS 15.0, *)
extension Text {
// MARK: - Initializer
/// Renders the given string containing HTML-tags with the related formatting.
///
/// - SeeAlso: See type `HTMLToMarkdownConverter.Tags` for a list of supported HTML-tags.
init(html htmlAsString: String) {
let markdownAsString = HTMLToMarkdownConverter.convert(htmlAsString)
do {
let markdownAsAttributedString = try AttributedString(markdown: markdownAsString)
self = .init(markdownAsAttributedString)
} catch {
print("⚠️ – Couldn't parse markdown: \(error)")
// Show the "plain" markdown string as a fallback.
self = .init(markdownAsString)
}
}
}
@fxm90
Copy link
Author

fxm90 commented Jun 28, 2021

Example

Code

struct ContentView: View {
  var body: some View {
    VStack(spacing: 4) {
      Text(html: "Regular")
      Text(html: "<em>Italics</em>")
      Text(html: "<strong>Bold</strong>")
      Text(html: "<s>Strikethrough</s>")
      Text(html: "<code>Code (Monospaced)</code>")
      Text(html: "<a href=\"https://apple.com\">Visit Apple</a>")
      Text(html: "<strong><em><a href=\"https://apple.com\">They</a></em> <s>are</s> <code>combinable</code>.</strong>")
    }
    .padding()
  }
}

#Preview {
  ContentView()
}

Preview

image

@fxm90
Copy link
Author

fxm90 commented Jun 28, 2021

Related test when using XCTest

import XCTest

final class HTMLToMarkdownConverterTestCase: XCTestCase {

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

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

    // Then
    XCTAssertEqual(html, markdown)
  }

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

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

    // Then
    XCTAssertEqual(html, markdown)
  }

  func test_convert_shouldReplaceStrongTag() {
    // Given
    let html = "<strong>Lorem</strong> ipsum <strong>dolor</strong> sit <strong>amet</strong>"

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

    // Then
    XCTAssertEqual(markdown, "**Lorem** ipsum **dolor** sit **amet**")
  }

  func test_convert_shouldReplaceEmTag() {
    // Given
    let html = "<em>Lorem</em> ipsum <em>dolor</em> sit <em>amet</em>"

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

    // Then
    XCTAssertEqual(markdown, "*Lorem* ipsum *dolor* sit *amet*")
  }

  func test_convert_shouldReplaceStrikeTag() {
    // Given
    let html = "<s>Lorem</s> ipsum <s>dolor</s> sit <s>amet</s>"

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

    // Then
    XCTAssertEqual(markdown, "~~Lorem~~ ipsum ~~dolor~~ sit ~~amet~~")
  }

  func test_convert_shouldReplaceCodeTag() {
    // Given
    let html = "<code>Lorem</code> ipsum <code>dolor</code> sit <code>amet</code>"

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

    // Then
    XCTAssertEqual(markdown, "`Lorem` ipsum `dolor` sit `amet`")
  }

  func test_convert_shouldReplaceLinkTag() {
    // Given
    let html = "Visit <a href=\"https://apple.com\">Apple</a>"

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

    // Then
    XCTAssertEqual(markdown, "Visit [Apple](https://apple.com)")
  }

  func test_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
    XCTAssertEqual(markdown, "Visit [Apple](https://apple.com). **Lorem** *ipsum*")
  }

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

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

    // Then
    XCTAssertEqual(markdown, "Visit ***[Apple](https://apple.com)***")
  }
}

@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