import Foundation /// Renders inline org content to `AttributedString` — the native counterpart to the HTML /// renderer, walking the same tree. /// /// This is the prototype's argument: a second output format is a walk over the parsed tree, /// not a second parser. It stays Foundation-only (no SwiftUI), so it is usable anywhere; a /// SwiftUI block renderer would sit on top, using this for the inline runs inside each block. public struct OrgAttributedStringRenderer: Sendable { /// Resolution options, so repository-relative links resolve exactly as they do in HTML. public var options: OrgRenderOptions public init(options: OrgRenderOptions = .init()) { self.options = options } private var resolver: OrgURLResolver { OrgURLResolver(options: options) } /// Render one run of inline objects, carrying intents a UI layer can style. /// /// Footnote numbering is document-wide, so a caller rendering a whole document threads /// its own ``OrgFootnotes`` through ``inline(_:_:)``; this overload numbers within the /// run it is given. public func inline(_ objects: [OrgObject]) -> AttributedString { var notes = OrgFootnotes(document: OrgDocument()) return inline(objects, ¬es) } package func inline(_ objects: [OrgObject], _ notes: inout OrgFootnotes) -> AttributedString { var result = AttributedString() for object in objects { switch object { case .text(let text): result += AttributedString(text) case .bold(let children): result += applying(.stronglyEmphasized, to: inline(children, ¬es)) case .italic(let children): result += applying(.emphasized, to: inline(children, ¬es)) // Underline and strikethrough have no Foundation-portable attribute (the // underlineStyle/strikethroughStyle keys live in the UIKit/AppKit scopes), so they // travel as roles the UI layer applies. case .underline(let children): result += applying(.underline, to: inline(children, ¬es)) case .strikeThrough(let children): result += applying(.strikeThrough, to: inline(children, ¬es)) case .verbatim(let text), .code(let text): result += applying(.code, to: AttributedString(text)) case .superscript(let children): // No portable superscript attribute; mark it so a UI layer can raise it. result += applying(.superscript, to: inline(children, ¬es)) case .lineBreak: result += AttributedString("\n") case .timestamp(let stamp): var part = AttributedString(stamp.displayValue) part.orgRole = .timestamp result += part case .footnoteRef(let label, let inlineNote): var part = AttributedString("[\(notes.number(for: label, inline: inlineNote))]") part.orgRole = .footnoteReference result += part case .image(let figure): var part = AttributedString(figure.alt ?? figure.source) part.orgRole = .image result += part case .link(let link): var part = link.description.map { inline($0, ¬es) } ?? AttributedString(resolver.displayValue(link.target)) if let href = resolver.href(for: link.target), let url = URL(string: href) { part.link = url } result += part } } return result } // MARK: - Nesting /// Add an intent to every run, keeping intents the children already carry. Assigning /// over the whole range instead would flatten `*bold /italic/*` to bold alone. private func applying( _ intent: InlinePresentationIntent, to string: AttributedString ) -> AttributedString { var result = string for range in result.runs.map(\.range) { result[range].inlinePresentationIntent = (result[range].inlinePresentationIntent ?? []).union(intent) } return result } /// Mark runs that carry no role yet. A role is a single slot, so the innermost — the more /// specific — wins; org nests two role-bearing markups (`_+both+_`) rarely enough that a /// set is not worth the public API. private func applying(_ role: OrgRole, to string: AttributedString) -> AttributedString { var result = string for range in result.runs.map(\.range) where result[range][OrgRoleAttribute.self] == nil { result[range][OrgRoleAttribute.self] = role } return result } /// Flatten a whole document to attributed paragraphs — a convenience for callers that /// want text without building block views (a share sheet, a plain-text export). public func paragraphs(_ document: OrgDocument) -> [AttributedString] { document.elements.compactMap { element in switch element { case .paragraph(let objects): return inline(objects) case .heading(let heading): return inline(heading.title) default: return nil } } } } // MARK: - Custom attribute /// Org roles that `AttributedString` has no standard attribute for. A UI layer reads these to /// decide presentation (raise a superscript, tint a timestamp, make a footnote ref tappable) /// without the renderer needing to know about fonts or colors. public enum OrgRole: String, Sendable, Codable { case superscript case timestamp case footnoteReference case image case underline case strikeThrough } public enum OrgRoleAttribute: AttributedStringKey { public typealias Value = OrgRole public static let name = "orgRole" } public extension AttributeScopes { struct OrgAttributes: AttributeScope { public let orgRole: OrgRoleAttribute } var org: OrgAttributes.Type { OrgAttributes.self } } public extension AttributedString { var orgRole: OrgRole? { get { self[OrgRoleAttribute.self] } set { self[OrgRoleAttribute.self] = newValue } } }