51 lines
2.0 KiB
Swift
51 lines
2.0 KiB
Swift
// Message text as styled text. Inline markdown (bold, italics, inline code, links) goes through
|
|
// `AttributedString(markdown:)`. Block syntax is rewritten line by line first, since SwiftUI's Text
|
|
// runs full-parsed paragraphs together: headings turn bold, list markers turn into bullets, quote
|
|
// markers go, and fenced code keeps its lines in monospace.
|
|
|
|
import Foundation
|
|
|
|
enum Markdown {
|
|
static func render(_ source: String) -> AttributedString {
|
|
var result = AttributedString()
|
|
var fenced = false
|
|
var first = true
|
|
for line in source.split(separator: "\n", omittingEmptySubsequences: false).map(String.init) {
|
|
if line.trimmingCharacters(in: .whitespaces).hasPrefix("```") {
|
|
fenced.toggle()
|
|
continue
|
|
}
|
|
if !first { result += AttributedString("\n") }
|
|
first = false
|
|
if fenced {
|
|
var code = AttributedString(line)
|
|
code.inlinePresentationIntent = .code
|
|
result += code
|
|
} else {
|
|
result += inline(block(line))
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
/// One line with its block syntax rewritten as inline markdown or plain text.
|
|
static func block(_ line: String) -> String {
|
|
if let heading = line.wholeMatch(of: /\s{0,3}#{1,6}\s+(.+?)[\s#]*/) {
|
|
return "**\(heading.1)**"
|
|
}
|
|
if let item = line.wholeMatch(of: /(\s*)[-*+]\s+(.*)/) {
|
|
return "\(item.1)• \(item.2)"
|
|
}
|
|
if let quote = line.wholeMatch(of: /\s{0,3}>\s?(.*)/) {
|
|
return String(quote.1)
|
|
}
|
|
return line
|
|
}
|
|
|
|
/// Inline markdown, or the line as plain text when it does not parse.
|
|
private static func inline(_ line: String) -> AttributedString {
|
|
let options = AttributedString.MarkdownParsingOptions(interpretedSyntax: .inlineOnlyPreservingWhitespace)
|
|
return (try? AttributedString(markdown: line, options: options)) ?? AttributedString(line)
|
|
}
|
|
}
|