From 9b2ab569d70700e2349a902c400b7b66258ee547 Mon Sep 17 00:00:00 2001 From: Jun Yan Date: Tue, 7 Jul 2026 14:40:44 -0700 Subject: [PATCH 1/2] Make font-holding config structs Sendable via SendableFont wrapper Introduce an internal SendableFont value type that centralizes the @unchecked Sendable assertion for MDFont (UIFont/NSFont). TextFonts, MarkdownInlineTextStyle, and CitationConfig now store SendableFont privately and expose their existing public MDFont API via computed accessors, so they are plainly Sendable without a per-struct unchecked assertion and without leaking a retroactive Sendable conformance onto the platform font types. Closes #124 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Models/MarkdownRenderConfig.swift | 17 +++++++----- Sources/MarkdownText/Style/TextFonts.swift | 21 +++++++++------ .../MarkdownText/Utilities/SendableFont.swift | 26 +++++++++++++++++++ 3 files changed, 50 insertions(+), 14 deletions(-) create mode 100644 Sources/MarkdownText/Utilities/SendableFont.swift diff --git a/Sources/MarkdownText/Models/MarkdownRenderConfig.swift b/Sources/MarkdownText/Models/MarkdownRenderConfig.swift index 20620fe..21e16cb 100644 --- a/Sources/MarkdownText/Models/MarkdownRenderConfig.swift +++ b/Sources/MarkdownText/Models/MarkdownRenderConfig.swift @@ -111,11 +111,11 @@ public struct MarkdownRenderConfig: Hashable, Sendable { /// Foreground color applied to bold-emphasis runs. public let boldTextColor: Color /// Font used for link runs. - public let linkTextFont: MDFont + public var linkTextFont: MDFont { _linkTextFont.font } /// Foreground color applied to link runs. public let linkTextColor: Color /// Font used for inline code spans. - public let codeTextFont: MDFont + public var codeTextFont: MDFont { _codeTextFont.font } /// Foreground color applied to inline code spans. public let codeTextColor: Color /// Background fill behind inline code spans. @@ -123,12 +123,15 @@ public struct MarkdownRenderConfig: Hashable, Sendable { /// Underline color drawn beneath inline code spans. public let codeUnderlineColor: Color + private let _linkTextFont: SendableFont + private let _codeTextFont: SendableFont + /// Create an inline text style with the supplied fonts and color palette. public init(boldTextColor: Color, linkTextFont: MDFont, linkTextColor: Color, codeTextFont: MDFont, codeTextColor: Color, codeBackgroundColor: Color, codeUnderlineColor: Color) { self.boldTextColor = boldTextColor - self.linkTextFont = linkTextFont + self._linkTextFont = SendableFont(linkTextFont) self.linkTextColor = linkTextColor - self.codeTextFont = codeTextFont + self._codeTextFont = SendableFont(codeTextFont) self.codeTextColor = codeTextColor self.codeBackgroundColor = codeBackgroundColor self.codeUnderlineColor = codeUnderlineColor @@ -142,12 +145,14 @@ public struct MarkdownRenderConfig: Hashable, Sendable { /// Encoder/decoder used to embed citation payloads into the markdown. public let coder: CitationCoder /// Font applied to the rendered citation chip. - public let font: MDFont + public var font: MDFont { _font.font } /// Foreground color of the citation chip text. public let textColor: Color /// Background fill of the citation chip. public let backgroundColor: Color + private let _font: SendableFont + /// Create a citation configuration. /// - Parameters: /// - isEnabled: See `isEnabled`. Defaults to `true`. @@ -164,7 +169,7 @@ public struct MarkdownRenderConfig: Hashable, Sendable { ) { self.isEnabled = isEnabled self.coder = coder - self.font = font + self._font = SendableFont(font) self.textColor = textColor self.backgroundColor = backgroundColor } diff --git a/Sources/MarkdownText/Style/TextFonts.swift b/Sources/MarkdownText/Style/TextFonts.swift index e8ef08e..18af395 100644 --- a/Sources/MarkdownText/Style/TextFonts.swift +++ b/Sources/MarkdownText/Style/TextFonts.swift @@ -10,25 +10,30 @@ import SwiftUI /// to style a run of text. public struct TextFonts: Hashable, Sendable { /// Regular variant. Always required. - public let normal: MDFont + public var normal: MDFont { _normal.font } /// Italic variant, or `nil` to fall back to `normal` for emphasis. - public let italic: MDFont? + public var italic: MDFont? { _italic?.font } /// Bold variant, or `nil` to fall back to `normal` for strong runs. - public let bold: MDFont? + public var bold: MDFont? { _bold?.font } /// Bold-italic variant, or `nil` to fall back to `bold` then `italic`. - public let boldItalic: MDFont? + public var boldItalic: MDFont? { _boldItalic?.font } /// Optional kerning override applied via `NSAttributedString.Key.kern`. public let preferredLetterSpacing: CGFloat? /// Optional preferred line height in points. When greater than the font's /// natural line height, the renderer adds the difference as line spacing. public let preferredLineHeight: CGFloat? + private let _normal: SendableFont + private let _italic: SendableFont? + private let _bold: SendableFont? + private let _boldItalic: SendableFont? + /// Create a font set with explicit variants and optional spacing overrides. public init(normal: MDFont, italic: MDFont?, bold: MDFont?, boldItalic: MDFont?, preferredLetterSpacing: CGFloat?, preferredLineHeight: CGFloat?) { - self.normal = normal - self.italic = italic - self.bold = bold - self.boldItalic = boldItalic + self._normal = SendableFont(normal) + self._italic = italic.map(SendableFont.init) + self._bold = bold.map(SendableFont.init) + self._boldItalic = boldItalic.map(SendableFont.init) self.preferredLetterSpacing = preferredLetterSpacing self.preferredLineHeight = preferredLineHeight } diff --git a/Sources/MarkdownText/Utilities/SendableFont.swift b/Sources/MarkdownText/Utilities/SendableFont.swift new file mode 100644 index 0000000..fef9bd3 --- /dev/null +++ b/Sources/MarkdownText/Utilities/SendableFont.swift @@ -0,0 +1,26 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. +// + +#if canImport(UIKit) +import UIKit +#elseif canImport(AppKit) +import AppKit +#endif + +/// A `Sendable` wrapper around `MDFont` (`UIFont`/`NSFont`). +/// +/// `UIFont`/`NSFont` are effectively immutable and safe to share across +/// concurrency domains, but the SDK does not mark them as `Sendable`. Storing a +/// font in this value type keeps the unchecked assertion in a single, auditable +/// place so font-holding configuration types can be plainly `Sendable` without +/// leaking a retroactive `Sendable` conformance onto the platform font types. +struct SendableFont: Hashable, @unchecked Sendable { + /// The wrapped platform font. + let font: MDFont + + init(_ font: MDFont) { + self.font = font + } +} From c53c82dc019d7be33c9bdbceb5d5c87e68428f67 Mon Sep 17 00:00:00 2001 From: Jun Yan Date: Tue, 7 Jul 2026 14:47:17 -0700 Subject: [PATCH 2/2] Rename private SendableFont storage to internal-prefixed names Use internal-prefixed property names (internalNormal, internalFont, etc.) instead of a leading underscore for the private SendableFont storage, per repo naming convention. Refs #124 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Models/MarkdownRenderConfig.swift | 18 +++++++------- Sources/MarkdownText/Style/TextFonts.swift | 24 +++++++++---------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Sources/MarkdownText/Models/MarkdownRenderConfig.swift b/Sources/MarkdownText/Models/MarkdownRenderConfig.swift index 21e16cb..2ee1dac 100644 --- a/Sources/MarkdownText/Models/MarkdownRenderConfig.swift +++ b/Sources/MarkdownText/Models/MarkdownRenderConfig.swift @@ -111,11 +111,11 @@ public struct MarkdownRenderConfig: Hashable, Sendable { /// Foreground color applied to bold-emphasis runs. public let boldTextColor: Color /// Font used for link runs. - public var linkTextFont: MDFont { _linkTextFont.font } + public var linkTextFont: MDFont { internalLinkTextFont.font } /// Foreground color applied to link runs. public let linkTextColor: Color /// Font used for inline code spans. - public var codeTextFont: MDFont { _codeTextFont.font } + public var codeTextFont: MDFont { internalCodeTextFont.font } /// Foreground color applied to inline code spans. public let codeTextColor: Color /// Background fill behind inline code spans. @@ -123,15 +123,15 @@ public struct MarkdownRenderConfig: Hashable, Sendable { /// Underline color drawn beneath inline code spans. public let codeUnderlineColor: Color - private let _linkTextFont: SendableFont - private let _codeTextFont: SendableFont + private let internalLinkTextFont: SendableFont + private let internalCodeTextFont: SendableFont /// Create an inline text style with the supplied fonts and color palette. public init(boldTextColor: Color, linkTextFont: MDFont, linkTextColor: Color, codeTextFont: MDFont, codeTextColor: Color, codeBackgroundColor: Color, codeUnderlineColor: Color) { self.boldTextColor = boldTextColor - self._linkTextFont = SendableFont(linkTextFont) + self.internalLinkTextFont = SendableFont(linkTextFont) self.linkTextColor = linkTextColor - self._codeTextFont = SendableFont(codeTextFont) + self.internalCodeTextFont = SendableFont(codeTextFont) self.codeTextColor = codeTextColor self.codeBackgroundColor = codeBackgroundColor self.codeUnderlineColor = codeUnderlineColor @@ -145,13 +145,13 @@ public struct MarkdownRenderConfig: Hashable, Sendable { /// Encoder/decoder used to embed citation payloads into the markdown. public let coder: CitationCoder /// Font applied to the rendered citation chip. - public var font: MDFont { _font.font } + public var font: MDFont { internalFont.font } /// Foreground color of the citation chip text. public let textColor: Color /// Background fill of the citation chip. public let backgroundColor: Color - private let _font: SendableFont + private let internalFont: SendableFont /// Create a citation configuration. /// - Parameters: @@ -169,7 +169,7 @@ public struct MarkdownRenderConfig: Hashable, Sendable { ) { self.isEnabled = isEnabled self.coder = coder - self._font = SendableFont(font) + self.internalFont = SendableFont(font) self.textColor = textColor self.backgroundColor = backgroundColor } diff --git a/Sources/MarkdownText/Style/TextFonts.swift b/Sources/MarkdownText/Style/TextFonts.swift index 18af395..b832b2e 100644 --- a/Sources/MarkdownText/Style/TextFonts.swift +++ b/Sources/MarkdownText/Style/TextFonts.swift @@ -10,30 +10,30 @@ import SwiftUI /// to style a run of text. public struct TextFonts: Hashable, Sendable { /// Regular variant. Always required. - public var normal: MDFont { _normal.font } + public var normal: MDFont { internalNormal.font } /// Italic variant, or `nil` to fall back to `normal` for emphasis. - public var italic: MDFont? { _italic?.font } + public var italic: MDFont? { internalItalic?.font } /// Bold variant, or `nil` to fall back to `normal` for strong runs. - public var bold: MDFont? { _bold?.font } + public var bold: MDFont? { internalBold?.font } /// Bold-italic variant, or `nil` to fall back to `bold` then `italic`. - public var boldItalic: MDFont? { _boldItalic?.font } + public var boldItalic: MDFont? { internalBoldItalic?.font } /// Optional kerning override applied via `NSAttributedString.Key.kern`. public let preferredLetterSpacing: CGFloat? /// Optional preferred line height in points. When greater than the font's /// natural line height, the renderer adds the difference as line spacing. public let preferredLineHeight: CGFloat? - private let _normal: SendableFont - private let _italic: SendableFont? - private let _bold: SendableFont? - private let _boldItalic: SendableFont? + private let internalNormal: SendableFont + private let internalItalic: SendableFont? + private let internalBold: SendableFont? + private let internalBoldItalic: SendableFont? /// Create a font set with explicit variants and optional spacing overrides. public init(normal: MDFont, italic: MDFont?, bold: MDFont?, boldItalic: MDFont?, preferredLetterSpacing: CGFloat?, preferredLineHeight: CGFloat?) { - self._normal = SendableFont(normal) - self._italic = italic.map(SendableFont.init) - self._bold = bold.map(SendableFont.init) - self._boldItalic = boldItalic.map(SendableFont.init) + self.internalNormal = SendableFont(normal) + self.internalItalic = italic.map(SendableFont.init) + self.internalBold = bold.map(SendableFont.init) + self.internalBoldItalic = boldItalic.map(SendableFont.init) self.preferredLetterSpacing = preferredLetterSpacing self.preferredLineHeight = preferredLineHeight }