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 { public init() {} /// Render one run of inline objects, carrying intents a UI layer can style. public func inline(_ objects: [OrgObject]) -> AttributedString { var result = AttributedString() for object in objects { switch object { case .text(let text): result += AttributedString(text) case .bold(let children): var part = inline(children) part.inlinePresentationIntent = .stronglyEmphasized result += part case .italic(let children): var part = inline(children) part.inlinePresentationIntent = .emphasized result += part // 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): var part = inline(children) part.orgRole = .underline result += part case .strikeThrough(let children): var part = inline(children) part.orgRole = .strikeThrough result += part case .verbatim(let text), .code(let text): var part = AttributedString(text) part.inlinePresentationIntent = .code result += part case .superscript(let children): // No portable superscript attribute; mark it so a UI layer can raise it. var part = inline(children) part.orgRole = .superscript result += part case .lineBreak: result += AttributedString("\n") case .timestamp(let stamp): var part = AttributedString(stamp.displayValue) part.orgRole = .timestamp result += part case .footnoteRef(let label, _): var part = AttributedString("[\(label)]") 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) } ?? AttributedString(displayValue(link.target)) if let url = URL(string: hrefValue(link.target)) { part.link = url } result += part } } 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 } } } private func hrefValue(_ target: OrgLinkTarget) -> String { switch target { case .external(let url): return url case .file(let path): return path case .id(let identifier): return "#\(identifier)" } } private func displayValue(_ target: OrgLinkTarget) -> String { switch target { case .external(let url): return url case .file(let path): return path case .id(let identifier): return identifier } } } // 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 } } }