Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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()))
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}

}
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
@@ -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())
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading