diff --git a/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/DefaultMarkdownTheme.swift b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/DefaultMarkdownTheme.swift new file mode 100644 index 000000000..e79d1ae53 --- /dev/null +++ b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/DefaultMarkdownTheme.swift @@ -0,0 +1,18 @@ +import SwiftUI + +private typealias Semantic = ThemeColorSpec.SemanticColor + +enum DefaultMarkdownTheme { + static func make() -> MarkdownTheme { + MarkdownTheme { + Paragraph() + .font(.body) + .foregroundStyle(Semantic.primary) + .lineHeight(26) + .marginBottom(16) + + Strong() + Emphasis() + } + } +} diff --git a/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/Elements/Emphasis.swift b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/Elements/Emphasis.swift new file mode 100644 index 000000000..a6e1e2d21 --- /dev/null +++ b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/Elements/Emphasis.swift @@ -0,0 +1,18 @@ +import SwiftUI + +public struct Emphasis: MarkdownThemeElement { + public var fontSpec: ThemeFontSpec? + public var fontWeight: Font.Weight? + public var fontDesign: Font.Design? + public var foregroundColorSpec: ThemeColorSpec? + public var marginTop: CGFloat? + public var marginBottom: CGFloat? + public var lineHeight: CGFloat? + public var textAlignment: TextAlignment? + + public init() {} + + public func apply(to config: inout MarkdownStyleConfig, traitCollection: UITraitCollection) { + applyElementStyle(to: &config.emphasis, traitCollection: traitCollection) + } +} diff --git a/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/Elements/Paragraph.swift b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/Elements/Paragraph.swift new file mode 100644 index 000000000..c0c73ccc8 --- /dev/null +++ b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/Elements/Paragraph.swift @@ -0,0 +1,18 @@ +import SwiftUI + +public struct Paragraph: MarkdownThemeElement { + public var fontSpec: ThemeFontSpec? + public var fontWeight: Font.Weight? + public var fontDesign: Font.Design? + public var foregroundColorSpec: ThemeColorSpec? + public var marginTop: CGFloat? + public var marginBottom: CGFloat? + public var lineHeight: CGFloat? + public var textAlignment: TextAlignment? + + public init() {} + + public func apply(to config: inout MarkdownStyleConfig, traitCollection: UITraitCollection) { + applyElementStyle(to: &config.paragraph, traitCollection: traitCollection) + } +} diff --git a/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/Elements/Strong.swift b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/Elements/Strong.swift new file mode 100644 index 000000000..45aa1e9fe --- /dev/null +++ b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/Elements/Strong.swift @@ -0,0 +1,18 @@ +import SwiftUI + +public struct Strong: MarkdownThemeElement { + public var fontSpec: ThemeFontSpec? + public var fontWeight: Font.Weight? + public var fontDesign: Font.Design? + public var foregroundColorSpec: ThemeColorSpec? + public var marginTop: CGFloat? + public var marginBottom: CGFloat? + public var lineHeight: CGFloat? + public var textAlignment: TextAlignment? + + public init() {} + + public func apply(to config: inout MarkdownStyleConfig, traitCollection: UITraitCollection) { + applyElementStyle(to: &config.strong, traitCollection: traitCollection) + } +} diff --git a/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/Environment+MarkdownTheme.swift b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/Environment+MarkdownTheme.swift new file mode 100644 index 000000000..a7c945e88 --- /dev/null +++ b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/Environment+MarkdownTheme.swift @@ -0,0 +1,31 @@ +import SwiftUI + +private struct MarkdownThemeLayersKey: EnvironmentKey { + static let defaultValue: [MarkdownTheme] = [.default] +} + +public extension EnvironmentValues { + var markdownThemeLayers: [MarkdownTheme] { + get { self[MarkdownThemeLayersKey.self] } + set { self[MarkdownThemeLayersKey.self] = newValue } + } +} + +private struct MarkdownThemeLayerModifier: ViewModifier { + @Environment(\.markdownThemeLayers) private var parentLayers + let theme: MarkdownTheme + + func body(content: Content) -> some View { + content.environment(\.markdownThemeLayers, parentLayers + [theme]) + } +} + +public extension View { + func markdownTheme(_ theme: MarkdownTheme) -> some View { + modifier(MarkdownThemeLayerModifier(theme: theme)) + } + + func markdownTheme(@MarkdownThemeBuilder _ content: () -> MarkdownThemeGroup) -> some View { + markdownTheme(MarkdownTheme(content: content())) + } +} diff --git a/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/MarkdownStyleConfig+Resolve.swift b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/MarkdownStyleConfig+Resolve.swift new file mode 100644 index 000000000..ba87b3d19 --- /dev/null +++ b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/MarkdownStyleConfig+Resolve.swift @@ -0,0 +1,18 @@ +import UIKit + +extension MarkdownStyleConfig { + public static func resolve( + layers: [MarkdownTheme], + traitCollection: UITraitCollection + ) -> MarkdownStyleConfig { + var config = MarkdownStyleConfig() + for layer in layers { + layer.apply(to: &config, traitCollection: traitCollection) + } + return config + } + + public static func baseline(traitCollection: UITraitCollection = .current) -> MarkdownStyleConfig { + resolve(layers: [.default], traitCollection: traitCollection) + } +} diff --git a/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/MarkdownStyleConfig.swift b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/MarkdownStyleConfig.swift new file mode 100644 index 000000000..a4ee160e3 --- /dev/null +++ b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/MarkdownStyleConfig.swift @@ -0,0 +1,58 @@ +import UIKit + +public struct ElementStyle: Equatable, Sendable { + public var font: UIFont? + public var foregroundColor: UIColor? + public var marginTop: CGFloat? + public var marginBottom: CGFloat? + public var lineHeight: CGFloat? + public var textAlignment: NSTextAlignment? + + public init( + font: UIFont? = nil, + foregroundColor: UIColor? = nil, + marginTop: CGFloat? = nil, + marginBottom: CGFloat? = nil, + lineHeight: CGFloat? = nil, + textAlignment: NSTextAlignment? = nil + ) { + self.font = font + self.foregroundColor = foregroundColor + self.marginTop = marginTop + self.marginBottom = marginBottom + self.lineHeight = lineHeight + self.textAlignment = textAlignment + } + + public mutating func merge(_ other: ElementStyle) { + if let font = other.font { self.font = font } + if let foregroundColor = other.foregroundColor { self.foregroundColor = foregroundColor } + if let marginTop = other.marginTop { self.marginTop = marginTop } + if let marginBottom = other.marginBottom { self.marginBottom = marginBottom } + if let lineHeight = other.lineHeight { self.lineHeight = lineHeight } + if let textAlignment = other.textAlignment { self.textAlignment = textAlignment } + } +} + +public struct MarkdownStyleConfig: Equatable, Sendable { + public var paragraph: ElementStyle + public var strong: ElementStyle + public var emphasis: ElementStyle + + public init( + paragraph: ElementStyle = ElementStyle(), + strong: ElementStyle = ElementStyle(), + emphasis: ElementStyle = ElementStyle() + ) { + self.paragraph = paragraph + self.strong = strong + self.emphasis = emphasis + } + + public mutating func merge(_ other: MarkdownStyleConfig) { + paragraph.merge(other.paragraph) + strong.merge(other.strong) + emphasis.merge(other.emphasis) + } + +} diff --git a/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/MarkdownTheme.swift b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/MarkdownTheme.swift new file mode 100644 index 000000000..479dac946 --- /dev/null +++ b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/MarkdownTheme.swift @@ -0,0 +1,20 @@ +import SwiftUI +import UIKit + +public struct MarkdownTheme: Sendable { + private let content: any MarkdownThemeContent + + public init(@MarkdownThemeBuilder _ content: () -> MarkdownThemeGroup) { + self.content = content() + } + + init(content: any MarkdownThemeContent) { + self.content = content + } + + func apply(to config: inout MarkdownStyleConfig, traitCollection: UITraitCollection) { + content.apply(to: &config, traitCollection: traitCollection) + } + + public static let `default` = DefaultMarkdownTheme.make() +} diff --git a/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/MarkdownThemeBuilder.swift b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/MarkdownThemeBuilder.swift new file mode 100644 index 000000000..188e39ad5 --- /dev/null +++ b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/MarkdownThemeBuilder.swift @@ -0,0 +1,25 @@ +@resultBuilder +public enum MarkdownThemeBuilder { + public static func buildBlock(_ components: any MarkdownThemeContent...) -> MarkdownThemeGroup { + MarkdownThemeGroup(contents: components) + } + + public static func buildOptional(_ component: (any MarkdownThemeContent)?) -> MarkdownThemeGroup { + if let component { + return MarkdownThemeGroup(contents: [component]) + } + return MarkdownThemeGroup(contents: []) + } + + public static func buildEither(first component: any MarkdownThemeContent) -> MarkdownThemeGroup { + MarkdownThemeGroup(contents: [component]) + } + + public static func buildEither(second component: any MarkdownThemeContent) -> MarkdownThemeGroup { + MarkdownThemeGroup(contents: [component]) + } + + public static func buildArray(_ components: [any MarkdownThemeContent]) -> MarkdownThemeGroup { + MarkdownThemeGroup(contents: components) + } +} diff --git a/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/MarkdownThemeContent.swift b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/MarkdownThemeContent.swift new file mode 100644 index 000000000..ca8597a38 --- /dev/null +++ b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/MarkdownThemeContent.swift @@ -0,0 +1,19 @@ +import UIKit + +public protocol MarkdownThemeContent { + func apply(to config: inout MarkdownStyleConfig, traitCollection: UITraitCollection) +} + +public struct MarkdownThemeGroup: MarkdownThemeContent, Sendable { + let contents: [any MarkdownThemeContent] + + public init(contents: [any MarkdownThemeContent]) { + self.contents = contents + } + + public func apply(to config: inout MarkdownStyleConfig, traitCollection: UITraitCollection) { + for content in contents { + content.apply(to: &config, traitCollection: traitCollection) + } + } +} diff --git a/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/MarkdownThemeElement.swift b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/MarkdownThemeElement.swift new file mode 100644 index 000000000..cbac8efc3 --- /dev/null +++ b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/MarkdownThemeElement.swift @@ -0,0 +1,93 @@ +import SwiftUI +import UIKit + +public protocol MarkdownThemeElement: MarkdownThemeContent { + var fontSpec: ThemeFontSpec? { get set } + var fontWeight: Font.Weight? { get set } + var fontDesign: Font.Design? { get set } + var foregroundColorSpec: ThemeColorSpec? { get set } + var marginTop: CGFloat? { get set } + var marginBottom: CGFloat? { get set } + var lineHeight: CGFloat? { get set } + var textAlignment: TextAlignment? { get set } +} + +public extension MarkdownThemeElement { + func font(_ font: Font) -> Self { + var copy = self + copy.fontSpec = ThemeResolver.font(from: font, traitCollection: .current) + return copy + } + + func bold() -> Self { + var copy = self + copy.fontWeight = .bold + return copy + } + + func foregroundStyle(_ color: Color) -> Self { + var copy = self + copy.foregroundColorSpec = ThemeResolver.color(from: color, traitCollection: .current) + return copy + } + + func foregroundStyle(_ semantic: ThemeColorSpec.SemanticColor) -> Self { + var copy = self + copy.foregroundColorSpec = .semantic(semantic) + return copy + } + + func marginTop(_ value: CGFloat) -> Self { + var copy = self + copy.marginTop = value + return copy + } + + func marginBottom(_ value: CGFloat) -> Self { + var copy = self + copy.marginBottom = value + return copy + } + + func lineHeight(_ value: CGFloat) -> Self { + var copy = self + copy.lineHeight = value + return copy + } + + func textAlignment(_ alignment: TextAlignment) -> Self { + var copy = self + copy.textAlignment = alignment + return copy + } + + func applyElementStyle( + to style: inout ElementStyle, + traitCollection: UITraitCollection + ) { + if fontSpec != nil || fontWeight != nil || fontDesign != nil { + style.font = ThemeResolver.applyFont( + spec: fontSpec, + weight: fontWeight, + design: fontDesign, + to: style.font, + traitCollection: traitCollection + ) + } + if let foregroundColorSpec { + style.foregroundColor = foregroundColorSpec.resolve(traitCollection: traitCollection) + } + if let marginTop { style.marginTop = marginTop } + if let marginBottom { style.marginBottom = marginBottom } + if let lineHeight { style.lineHeight = lineHeight } + if let textAlignment { style.textAlignment = nsTextAlignment(from: textAlignment) } + } + + private func nsTextAlignment(from alignment: TextAlignment) -> NSTextAlignment { + switch alignment { + case .leading: return .left + case .center: return .center + case .trailing: return .right + } + } +} diff --git a/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/RememberMarkdownTheme.swift b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/RememberMarkdownTheme.swift new file mode 100644 index 000000000..14431bd04 --- /dev/null +++ b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/RememberMarkdownTheme.swift @@ -0,0 +1,28 @@ +import SwiftUI + +/// Re-create a `MarkdownTheme` when `colorScheme` or `dynamicTypeSize` changes. +/// +/// Call from a `View.body` after reading those values from the environment: +/// +/// ```swift +/// @Environment(\.colorScheme) private var colorScheme +/// @Environment(\.dynamicTypeSize) private var dynamicTypeSize +/// +/// var body: some View { +/// let theme = rememberMarkdownTheme(colorScheme: colorScheme, dynamicTypeSize: dynamicTypeSize) { +/// Paragraph().foregroundStyle(.primary) +/// } +/// RootView() +/// .markdownTheme(theme) +/// } +/// ``` +@MainActor +public func rememberMarkdownTheme( + colorScheme: ColorScheme, + dynamicTypeSize: DynamicTypeSize, + @MarkdownThemeBuilder _ content: () -> MarkdownThemeGroup +) -> MarkdownTheme { + _ = colorScheme + _ = dynamicTypeSize + return MarkdownTheme(content: content()) +} diff --git a/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/ThemeColorModifiers.swift b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/ThemeColorModifiers.swift new file mode 100644 index 000000000..0bee899be --- /dev/null +++ b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/ThemeColorModifiers.swift @@ -0,0 +1,11 @@ +import SwiftUI + +enum ThemeColorModifiers { + static func spec(from color: Color) -> ThemeColorSpec { + ThemeResolver.color(from: color, traitCollection: .current) + } + + static func spec(from semantic: ThemeColorSpec.SemanticColor) -> ThemeColorSpec { + .semantic(semantic) + } +} diff --git a/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/ThemeResolver.swift b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/ThemeResolver.swift new file mode 100644 index 000000000..796f08411 --- /dev/null +++ b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Theme/ThemeResolver.swift @@ -0,0 +1,134 @@ +import SwiftUI +import UIKit + +public enum ThemeFontSpec: Equatable, Sendable { + case textStyle(UIFont.TextStyle) + case system(size: CGFloat, weight: UIFont.Weight, design: UIFontDescriptor.SystemDesign) + + func resolve(traitCollection: UITraitCollection) -> UIFont { + switch self { + case let .textStyle(style): + return UIFont.preferredFont(forTextStyle: style, compatibleWith: traitCollection) + case let .system(size, weight, design): + let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body, compatibleWith: traitCollection) + .withDesign(design)? + .addingAttributes([ + .traits: [UIFontDescriptor.TraitKey.weight: weight] + ]) ?? UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body, compatibleWith: traitCollection) + return UIFont(descriptor: descriptor, size: size) + } + } +} + +public enum ThemeColorSpec: Equatable, Sendable { + case semantic(SemanticColor) + case uiColor(UIColor) + + public enum SemanticColor: Equatable, Sendable { + case primary + case secondary + case tint + case quaternary + } + + func resolve(traitCollection: UITraitCollection) -> UIColor { + switch self { + case let .semantic(semantic): + switch semantic { + case .primary: + return UIColor.label.resolvedColor(with: traitCollection) + case .secondary: + return UIColor.secondaryLabel.resolvedColor(with: traitCollection) + case .tint: + return UIColor.tintColor.resolvedColor(with: traitCollection) + case .quaternary: + return UIColor.quaternaryLabel.resolvedColor(with: traitCollection) + } + case let .uiColor(color): + return color.resolvedColor(with: traitCollection) + } + } +} + +enum ThemeResolver { + static func font(from font: Font, traitCollection: UITraitCollection) -> ThemeFontSpec { + // Map common SwiftUI text styles to UIFont.TextStyle + if font == .body { return .textStyle(.body) } + if font == .callout { return .textStyle(.callout) } + if font == .caption { return .textStyle(.caption1) } + if font == .caption2 { return .textStyle(.caption2) } + if font == .footnote { return .textStyle(.footnote) } + if font == .headline { return .textStyle(.headline) } + if font == .subheadline { return .textStyle(.subheadline) } + if font == .title { return .textStyle(.title1) } + if font == .title2 { return .textStyle(.title2) } + if font == .title3 { return .textStyle(.title3) } + if font == .largeTitle { return .textStyle(.largeTitle) } + return .textStyle(.body) + } + + static func color(from color: Color, traitCollection: UITraitCollection) -> ThemeColorSpec { + let uiColor = UIColor(color) + return .uiColor(uiColor) + } + + static func applyFont( + spec: ThemeFontSpec?, + weight: Font.Weight?, + design: Font.Design?, + to base: UIFont?, + traitCollection: UITraitCollection + ) -> UIFont? { + guard let spec else { return base } + var font = spec.resolve(traitCollection: traitCollection) + + if let weight { + let uiWeight = uiFontWeight(from: weight) + font = font.withWeight(uiWeight) + } + + if let design { + let uiDesign = uiFontDesign(from: design) + if let descriptor = font.fontDescriptor.withDesign(uiDesign) { + font = UIFont(descriptor: descriptor, size: font.pointSize) + } + } + + return font + } + + private static func uiFontWeight(from weight: Font.Weight) -> UIFont.Weight { + switch weight { + case .ultraLight: return .ultraLight + case .thin: return .thin + case .light: return .light + case .regular: return .regular + case .medium: return .medium + case .semibold: return .semibold + case .bold: return .bold + case .heavy: return .heavy + case .black: return .black + default: return .regular + } + } + + private static func uiFontDesign(from design: Font.Design) -> UIFontDescriptor.SystemDesign { + switch design { + case .default: return .default + case .serif: return .serif + case .rounded: return .rounded + case .monospaced: return .monospaced + default: return .default + } + } +} + +private extension UIFont { + func withWeight(_ weight: UIFont.Weight) -> UIFont { + let traits: [UIFontDescriptor.TraitKey: Any] = [ + .weight: weight + ] + let descriptor = fontDescriptor.addingAttributes([.traits: traits]) + return UIFont(descriptor: descriptor, size: pointSize) + } +} diff --git a/packages/ios-enriched-markdown/Tests/EnrichedMarkdownTests/SemanticColorTests.swift b/packages/ios-enriched-markdown/Tests/EnrichedMarkdownTests/SemanticColorTests.swift new file mode 100644 index 000000000..3711b72fd --- /dev/null +++ b/packages/ios-enriched-markdown/Tests/EnrichedMarkdownTests/SemanticColorTests.swift @@ -0,0 +1,44 @@ +import UIKit +import XCTest +@testable import EnrichedMarkdown + +final class SemanticColorTests: XCTestCase { + func testPrimaryResolvesToLabelInLightAndDark() { + let theme = MarkdownTheme { + Paragraph().foregroundStyle(ThemeColorSpec.SemanticColor.primary) + } + + let lightTraits = UITraitCollection(userInterfaceStyle: .light) + let darkTraits = UITraitCollection(userInterfaceStyle: .dark) + + let lightConfig = MarkdownStyleConfig.resolve(layers: [theme], traitCollection: lightTraits) + let darkConfig = MarkdownStyleConfig.resolve(layers: [theme], traitCollection: darkTraits) + + XCTAssertEqual( + lightConfig.paragraph.foregroundColor, + UIColor.label.resolvedColor(with: lightTraits) + ) + XCTAssertEqual( + darkConfig.paragraph.foregroundColor, + UIColor.label.resolvedColor(with: darkTraits) + ) + XCTAssertNotEqual( + lightConfig.paragraph.foregroundColor, + darkConfig.paragraph.foregroundColor + ) + } + + func testTintResolvesForLinkThemeElement() { + let theme = MarkdownTheme { + Link().foregroundStyle(ThemeColorSpec.SemanticColor.tint) + } + + let traits = UITraitCollection(userInterfaceStyle: .light) + let config = MarkdownStyleConfig.resolve(layers: [theme], traitCollection: traits) + + XCTAssertEqual( + config.link.foregroundColor, + UIColor.tintColor.resolvedColor(with: traits) + ) + } +} diff --git a/packages/ios-enriched-markdown/Tests/EnrichedMarkdownTests/ThemeResolutionTests.swift b/packages/ios-enriched-markdown/Tests/EnrichedMarkdownTests/ThemeResolutionTests.swift new file mode 100644 index 000000000..1019d8bbf --- /dev/null +++ b/packages/ios-enriched-markdown/Tests/EnrichedMarkdownTests/ThemeResolutionTests.swift @@ -0,0 +1,54 @@ +import SwiftUI +import UIKit +import XCTest +@testable import EnrichedMarkdown + +final class ThemeResolutionTests: XCTestCase { + func testLayeredThemesMergeDeclaredElementsOnly() { + let traitCollection = UITraitCollection(userInterfaceStyle: .light) + + let base = MarkdownTheme { + Paragraph() + .fontSize(18) + .foregroundStyle(ThemeColorSpec.SemanticColor.primary) + Heading(1) + .fontSize(30) + } + + let override = MarkdownTheme { + Heading(1) + .fontSize(40) + .foregroundStyle(ThemeColorSpec.SemanticColor.tint) + } + + let config = MarkdownStyleConfig.resolve( + layers: [base, override], + traitCollection: traitCollection + ) + + XCTAssertEqual(config.paragraph.font?.pointSize, 18) + XCTAssertEqual( + config.paragraph.foregroundColor, + UIColor.label.resolvedColor(with: traitCollection) + ) + XCTAssertEqual(config.heading1.font?.pointSize, 40) + XCTAssertEqual( + config.heading1.foregroundColor, + UIColor.tintColor.resolvedColor(with: traitCollection) + ) + } + + func testDefaultThemeResolvesAllCommonMarkElements() { + let config = MarkdownStyleConfig.baseline() + + XCTAssertNotNil(config.paragraph.font) + XCTAssertNotNil(config.paragraph.foregroundColor) + XCTAssertNotNil(config.heading1.font) + XCTAssertNotNil(config.link.foregroundColor) + XCTAssertTrue(config.link.underline == true) + XCTAssertNotNil(config.codeBlock.font) + XCTAssertNotNil(config.codeBlock.backgroundColor) + XCTAssertNotNil(config.blockquote.borderColor) + XCTAssertNotNil(config.list.bulletColor) + } +} diff --git a/packages/ios-enriched-markdown/Tests/EnrichedMarkdownTests/TraitCollectionTests.swift b/packages/ios-enriched-markdown/Tests/EnrichedMarkdownTests/TraitCollectionTests.swift new file mode 100644 index 000000000..6b23cd569 --- /dev/null +++ b/packages/ios-enriched-markdown/Tests/EnrichedMarkdownTests/TraitCollectionTests.swift @@ -0,0 +1,31 @@ +import SwiftUI +import UIKit +import XCTest +@testable import EnrichedMarkdown + +final class TraitCollectionTests: XCTestCase { + func testResolveProducesDifferentColorsAcrossColorSchemes() { + let lightTraits = ThemeResolver.traitCollection(colorScheme: .light, dynamicTypeSize: .large) + let darkTraits = ThemeResolver.traitCollection(colorScheme: .dark, dynamicTypeSize: .large) + + let lightConfig = MarkdownStyleConfig.resolve(layers: [.default], traitCollection: lightTraits) + let darkConfig = MarkdownStyleConfig.resolve(layers: [.default], traitCollection: darkTraits) + + XCTAssertNotEqual( + lightConfig.paragraph.foregroundColor, + darkConfig.paragraph.foregroundColor + ) + } + + func testTraitCollectionMapsDynamicTypeSize() { + let smallTraits = ThemeResolver.traitCollection(colorScheme: .light, dynamicTypeSize: .small) + let largeTraits = ThemeResolver.traitCollection(colorScheme: .light, dynamicTypeSize: .xxxLarge) + + let smallConfig = MarkdownStyleConfig.resolve(layers: [.default], traitCollection: smallTraits) + let largeConfig = MarkdownStyleConfig.resolve(layers: [.default], traitCollection: largeTraits) + + let smallSize = smallConfig.paragraph.font?.pointSize ?? 0 + let largeSize = largeConfig.paragraph.font?.pointSize ?? 0 + XCTAssertGreaterThan(largeSize, smallSize) + } +}