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 }