import OrgSwift import SwiftUI /// Turns a run of inline org objects into a styled `AttributedString`. /// /// The parse and the attribute intents come from ``OrgAttributedStringRenderer``; this adds /// only presentation. Everything is expressed in semantic `Font.TextStyle`s and semantic /// colors, so the result scales with Dynamic Type and follows light/dark on its own. struct OrgInlineStyler { var renderer: OrgAttributedStringRenderer init(options: OrgRenderOptions) { renderer = OrgAttributedStringRenderer(options: options) } /// - Parameter style: the text style of the block this run sits in, so emphasis inside a /// heading bolds the heading's font rather than the body font. /// - Parameters: /// - style: the text style of the block this run sits in, so emphasis inside a heading /// bolds the heading's font rather than the body font. /// - bold: forces every run bold, for headings. func attributed( _ objects: [OrgObject], _ notes: inout OrgFootnotes, style: Font.TextStyle = .body, bold forceBold: Bool = false ) -> AttributedString { var string = renderer.inline(objects, ¬es) for range in string.runs.map(\.range) { let intents = string[range].inlinePresentationIntent ?? [] let role = string[range][OrgRoleAttribute.self] var textStyle = style var monospaced = false var bold = forceBold var italic = false if intents.contains(.stronglyEmphasized) { bold = true } if intents.contains(.emphasized) { italic = true } if intents.contains(.code) { monospaced = true string[range].backgroundColor = .secondary.opacity(0.12) } switch role { case .underline: string[range].underlineStyle = .single case .strikeThrough: string[range].strikethroughStyle = .single case .superscript, .footnoteReference: // No portable superscript attribute, so raise it and step the style down. textStyle = Self.smaller(textStyle) string[range].baselineOffset = 5 if role == .footnoteReference { string[range].foregroundColor = .accentColor } case .timestamp: string[range].foregroundColor = .secondary case .image: // An inline image inside a paragraph shows its alt text; the block-level // `.figure` case is where an image is actually loaded. italic = true string[range].foregroundColor = .secondary case nil: break } // Always set the font, never only on modified runs: a run with no emphasis // would otherwise inherit the surrounding view's font and a heading, table cell // or footnote would render at body size. var font = Font.system(textStyle, design: monospaced ? .monospaced : .default) if bold { font = font.bold() } if italic { font = font.italic() } string[range].font = font } return string } /// One step down the semantic scale, for superscripts and footnote markers. private static func smaller(_ style: Font.TextStyle) -> Font.TextStyle { switch style { case .largeTitle: .title case .title: .title2 case .title2: .title3 case .title3: .headline case .headline, .body: .footnote case .callout: .caption case .subheadline: .caption case .footnote, .caption, .caption2: .caption2 @unknown default: .caption } } }