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.

Revisions

  1. fxm90 revised this gist Jan 27, 2026. 1 changed file with 104 additions and 82 deletions.
    186 changes: 104 additions & 82 deletions SwiftUI+HTML.swift
    Original file line number Diff line number Diff line change
    @@ -7,104 +7,126 @@

    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 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: - 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)
    }

    // 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)
    }
    // 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,
    )
    }
    }

    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)>"
    }
    // MARK: - Supporting Types

    var markdownTag: String {
    switch self {
    case .strong:
    return "**"
    extension HTMLToMarkdownConverter {

    case .em:
    return "*"
    /// 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)>"
    }

    case .s:
    return "~~"
    /// The closing HTML tag (e.g. `</strong>`).
    var closingHtmlTag: String {
    "</\(rawValue)>"
    }

    case .code:
    return "`"
    }
    }
    /// 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

    /// 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)
    }
    // 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)
    }
    }
    }
  2. fxm90 created this gist Jun 28, 2021.
    110 changes: 110 additions & 0 deletions SwiftUI+HTML.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,110 @@
    //
    // 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)
    }
    }
    }