From 98e21ab449ed1fa6d6dace1da2c3dee6a4b92ac2 Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Sat, 13 Sep 2025 17:38:00 -0700 Subject: [PATCH] Parse markdown oop --- Sources/Packages/Sources/Brief/Release.swift | 53 +++++++++++++++++++ .../XPCWrappers/XPCServiceDelegate.swift | 4 +- .../SecretiveUpdater/SecretiveUpdater.swift | 4 +- 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/Sources/Packages/Sources/Brief/Release.swift b/Sources/Packages/Sources/Brief/Release.swift index ffc3293..ebeedad 100644 --- a/Sources/Packages/Sources/Brief/Release.swift +++ b/Sources/Packages/Sources/Brief/Release.swift @@ -1,4 +1,5 @@ import Foundation +import SwiftUI /// A release is a representation of a downloadable update. public struct Release: Codable, Sendable { @@ -15,6 +16,8 @@ public struct Release: Codable, Sendable { /// A user-facing description of the contents of the update. public let body: String + public let attributedBody: AttributedString + /// Initializes a Release. /// - Parameters: /// - name: The user-facing name of the release. @@ -26,6 +29,56 @@ public struct Release: Codable, Sendable { self.prerelease = prerelease self.html_url = html_url self.body = body + self.attributedBody = AttributedString(_markdown: body) + } + + public init(_ release: GitHubRelease) { + self.name = release.name + self.prerelease = release.prerelease + self.html_url = release.html_url + self.body = release.body + self.attributedBody = AttributedString(_markdown: release.body) + } + +} + +public struct GitHubRelease: Codable, Sendable { + let name: String + let prerelease: Bool + let html_url: URL + let body: String +} + +fileprivate extension AttributedString { + + init(_markdown markdown: String) { + let split = markdown.split(whereSeparator: \.isNewline) + let lines = split + .compactMap { + try? AttributedString(markdown: String($0), options: .init(allowsExtendedAttributes: true, interpretedSyntax: .full)) + } + .map { (string: AttributedString) in + guard case let .header(level) = string.runs.first?.presentationIntent?.components.first?.kind else { return string } + return AttributedString("\n") + string + .transformingAttributes(\.font) { font in + font.value = switch level { + case 2: .title3.bold() + case 3: .title3 + default: .body + } + } + .transformingAttributes(\.underlineStyle) { underline in + underline.value = switch level { + case 2: .single + default: .none + } + } + + AttributedString("\n") + } + self = lines.reduce(into: AttributedString()) { partialResult, next in + partialResult.append(next) + partialResult.append(AttributedString("\n")) + } } } diff --git a/Sources/Packages/Sources/XPCWrappers/XPCServiceDelegate.swift b/Sources/Packages/Sources/XPCWrappers/XPCServiceDelegate.swift index 5108ed2..9fd9216 100644 --- a/Sources/Packages/Sources/XPCWrappers/XPCServiceDelegate.swift +++ b/Sources/Packages/Sources/XPCWrappers/XPCServiceDelegate.swift @@ -34,7 +34,9 @@ public final class XPCServiceDelegate: NSObject, NSXPCListenerDelegate { if let error = error as? Codable & Error { reply(nil, NSError(error)) } else { - reply(nil, error) + // Sending cast directly tries to serialize it and crashes XPCEncoder. + let cast = error as NSError + reply(nil, NSError(domain: cast.domain, code: cast.code, userInfo: [NSLocalizedDescriptionKey: error.localizedDescription])) } } } diff --git a/Sources/SecretiveUpdater/SecretiveUpdater.swift b/Sources/SecretiveUpdater/SecretiveUpdater.swift index 998fd31..eab2587 100644 --- a/Sources/SecretiveUpdater/SecretiveUpdater.swift +++ b/Sources/SecretiveUpdater/SecretiveUpdater.swift @@ -11,7 +11,9 @@ final class SecretiveUpdater: NSObject, XPCProtocol { func process(_: Data) async throws -> [Release] { let (data, _) = try await URLSession.shared.data(from: Constants.updateURL) - return try JSONDecoder().decode([Release].self, from: data) + return try JSONDecoder() + .decode([GitHubRelease].self, from: data) + .map(Release.init) } }