Files
wristcode/watch/WristCode/Views/Markdown.swift
T

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