damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

Markdown.swift (3017B)


      1 //
      2 //  Markdown.swift
      3 //  damus
      4 //
      5 //  Created by Lionello Lunesu on 2022-12-28.
      6 //
      7 
      8 import Foundation
      9 import SwiftUI
     10 
     11 func count_leading_hashes(_ str: String) -> Int {
     12     var count = 0
     13     for c in str {
     14         if c == "#" {
     15             count += 1
     16         } else {
     17             break
     18         }
     19     }
     20     
     21     return count
     22 }
     23 
     24 func get_heading_title_size(count: Int) -> SwiftUI.Font {
     25     if count >= 3 {
     26         return Font.title3
     27     } else if count >= 2 {
     28         return Font.title2
     29     } else if count >= 1 {
     30         return Font.title
     31     }
     32     
     33     return Font.body
     34 }
     35 
     36 public struct Markdown {
     37     private var detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
     38     
     39     /// Ensure the specified URL has a scheme by prepending "https://" if it's absent.
     40     static func withScheme(_ url: any StringProtocol) -> any StringProtocol {
     41         return url.contains("://") ? url : "https://" + url
     42     }
     43 
     44     /// Parse a string with markdown into an `AttributedString`, if possible, or else return it as regular text.
     45     public static func parse(content: String) -> AttributedString {
     46         let md_opts: AttributedString.MarkdownParsingOptions =
     47             .init(interpretedSyntax: .inlineOnlyPreservingWhitespace)
     48         
     49         guard content.utf8.count > 0 else {
     50             return AttributedString(stringLiteral: "")
     51         }
     52         
     53         let leading_hashes = count_leading_hashes(content)
     54         if leading_hashes > 0 {
     55             if var str = try? AttributedString(markdown: content) {
     56                 str.font = get_heading_title_size(count: leading_hashes)
     57                 return str
     58             }
     59         }
     60 
     61         // TODO: escape unintentional markdown
     62         let escaped = content.replacingOccurrences(of: "\\_", with: "\\\\\\_")
     63         if let txt = try? AttributedString(markdown: escaped, options: md_opts) {
     64             return txt
     65         } else {
     66             return AttributedString(stringLiteral: content)
     67         }
     68     }
     69 
     70     /// Process the input text and add markdown for any embedded URLs.
     71     public func process(_ input: String) -> AttributedString {
     72         guard let detector else {
     73             return AttributedString(stringLiteral: input)
     74         }
     75         let matches = detector.matches(in: input, options: [], range: NSRange(location: 0, length: input.utf16.count))
     76         var output = input
     77         // Start with the last match, because replacing the first would invalidate all subsequent indices
     78         for match in matches.reversed() {
     79             guard let range = Range(match.range, in: input)
     80                 , let url = match.url else { continue }
     81             let text = input[range]
     82             // Use the absoluteString from the matched URL, except when it defaults to http (since we default to https)
     83             let uri = url.scheme == "http" ? Markdown.withScheme(text) : url.absoluteString
     84             output.replaceSubrange(range, with: "[\(text)](\(uri))")
     85         }
     86         return Markdown.parse(content: output)
     87     }
     88 }