import Foundation // The org element tree — the document model, deliberately mirroring orgo's `model.rs` // (`Element`/`Object`, `TableRow::{Cells, Rule}`, `ListKind`, `Checkbox`) so the two can be // compared tree-to-tree later, instead of only through rendered HTML. // // Why a tree at all: the shipped renderer is a single pass from source straight to an HTML // string, so every new output format would mean re-deriving the parse. With a tree, parsing // happens once and each renderer is a walk — the HTML renderer stays the one the conformance // corpus measures, and a native renderer rides on the same proven parse. /// A parsed org document: metadata keywords plus the block elements of the body. public struct OrgDocument: Sendable, Equatable { /// `#+TITLE:`, `#+AUTHOR:`, `#+DATE:` and any other `#+KEY: value`, in source order. public var keywords: [(key: String, value: String)] public var elements: [OrgElement] public init(keywords: [(key: String, value: String)] = [], elements: [OrgElement] = []) { self.keywords = keywords self.elements = elements } public func keyword(_ name: String) -> String? { keywords.first { $0.key.caseInsensitiveCompare(name) == .orderedSame }?.value } public static func == (lhs: OrgDocument, rhs: OrgDocument) -> Bool { lhs.elements == rhs.elements && lhs.keywords.count == rhs.keywords.count && zip(lhs.keywords, rhs.keywords).allSatisfy { $0.key == $1.key && $0.value == $1.value } } } /// A block-level element. public indirect enum OrgElement: Sendable, Equatable { case heading(OrgHeading) case paragraph([OrgObject]) case list(OrgList) case table(OrgTable) case srcBlock(language: String?, code: String) case exampleBlock(String) case quoteBlock([OrgElement]) case centerBlock([OrgElement]) case verseBlock([[OrgObject]]) case specialBlock(name: String, content: [OrgElement]) case exportBlock(backend: String, raw: String) case figure(OrgFigure) /// A block carrying an affiliated `#+CAPTION:` / `#+NAME:`, which org's exporter wraps in /// a `
`. Images take the `.figure` case instead; this is for everything else /// (a captioned source block, example, or table). case captioned(name: String?, caption: [OrgObject]?, content: OrgElement) case horizontalRule case footnoteDefinition(label: String, content: [OrgObject]) } public struct OrgHeading: Sendable, Equatable { /// Star count, before any render-time level offset. public var level: Int public var todo: String? public var priority: Character? public var title: [OrgObject] public var tags: [String] public init(level: Int, todo: String? = nil, priority: Character? = nil, title: [OrgObject], tags: [String] = []) { self.level = level self.todo = todo self.priority = priority self.title = title self.tags = tags } } // MARK: - Lists public struct OrgList: Sendable, Equatable { public var kind: OrgListKind public var items: [OrgListItem] public init(kind: OrgListKind, items: [OrgListItem]) { self.kind = kind self.items = items } } public enum OrgListKind: Sendable, Equatable { case unordered case ordered case description } public struct OrgListItem: Sendable, Equatable { public var checkbox: OrgCheckbox? /// The term of a description-list item (`term :: definition`). public var term: [OrgObject]? /// The item's own content: one paragraph normally, several for a multi-paragraph item. public var content: [[OrgObject]] /// A nested list, when the item has one. public var sublist: OrgList? public init(checkbox: OrgCheckbox? = nil, term: [OrgObject]? = nil, content: [[OrgObject]], sublist: OrgList? = nil) { self.checkbox = checkbox self.term = term self.content = content self.sublist = sublist } } public enum OrgCheckbox: Sendable, Equatable { case off case on case partial } // MARK: - Tables public struct OrgTable: Sendable, Equatable { public var rows: [OrgTableRow] /// Per-column alignment from the separator row, when it carries `:` markers. public var alignments: [OrgAlignment?] public init(rows: [OrgTableRow], alignments: [OrgAlignment?] = []) { self.rows = rows self.alignments = alignments } /// A rule row separates the header band from the body, as in org. public var headerRowCount: Int { guard let ruleIndex = rows.firstIndex(where: { if case .rule = $0 { return true } else { return false } }) else { return 0 } return ruleIndex } } public enum OrgTableRow: Sendable, Equatable { case cells([[OrgObject]]) case rule } public enum OrgAlignment: String, Sendable, Equatable { case left, center, right } // MARK: - Figures public struct OrgFigure: Sendable, Equatable { public var source: String public var caption: [OrgObject]? /// `#+ATTR_HTML:` pairs, kept as parsed so an HTML renderer can emit them and a native /// renderer can read the ones it understands (`:alt`, `:width`). public var attributes: [(key: String, value: String)] public init(source: String, caption: [OrgObject]? = nil, attributes: [(key: String, value: String)] = []) { self.source = source self.caption = caption self.attributes = attributes } public var alt: String? { attributes.first { $0.key == "alt" }?.value } public static func == (lhs: OrgFigure, rhs: OrgFigure) -> Bool { lhs.source == rhs.source && lhs.caption == rhs.caption && lhs.attributes.count == rhs.attributes.count && zip(lhs.attributes, rhs.attributes).allSatisfy { $0.key == $1.key && $0.value == $1.value } } } // MARK: - Inline objects /// An inline object. Text-bearing cases carry their own children so a renderer can nest /// styling (`*bold /and italic/*`) rather than receiving pre-formatted markup. public indirect enum OrgObject: Sendable, Equatable { case text(String) case bold([OrgObject]) case italic([OrgObject]) case underline([OrgObject]) case strikeThrough([OrgObject]) /// Non-nesting by definition in org: `=verbatim=` and `~code~` hold literal text. case verbatim(String) case code(String) case link(OrgLink) case image(OrgFigure) /// A footnote reference. `inline` carries the text of an inline footnote /// (`[fn:label:text]`), which defines the note at the point of use; nil for a plain /// reference whose definition appears elsewhere. Numbering is document-wide, so it is the /// renderer's job, not the parser's. case footnoteRef(label: String, inline: [OrgObject]?) case timestamp(OrgTimestamp) case superscript([OrgObject]) case lineBreak } public struct OrgLink: Sendable, Equatable { public var target: OrgLinkTarget /// Nil description means the link shows its target. public var description: [OrgObject]? public init(target: OrgLinkTarget, description: [OrgObject]? = nil) { self.target = target self.description = description } } public enum OrgLinkTarget: Sendable, Equatable { /// An absolute URL (`https:`, `mailto:`) or a bare autolinked URL. case external(String) /// A repository-relative path, from `[[file:…]]` or a bare relative target. case file(String) /// `[[id:…]]` — an in-page fragment. case id(String) } public struct OrgTimestamp: Sendable, Equatable { public var date: String public var time: String? /// End of a same-day time range (`<… 10:00-11:45>`). public var endTime: String? /// End of a multi-day range (`--`), which org models as one timestamp with an end. public var endDate: String? public var active: Bool public init(date: String, time: String? = nil, endTime: String? = nil, endDate: String? = nil, active: Bool) { self.date = date self.time = time self.endTime = endTime self.endDate = endDate self.active = active } /// True when this stamp spans a range, in either form. public var isRange: Bool { endTime != nil || endDate != nil } /// The machine value and display text of the range's end, when there is one. A multi-day /// end shows its whole date; a same-day end shows only the time, since the date is already /// on the start. public var end: (machineValue: String, displayValue: String)? { if let endDate { return (endDate, endDate) } if let endTime { return ("\(date)T\(endTime)", endTime) } return nil } /// The `datetime` attribute value / sort key: `2024-01-15` or `2024-01-15T10:30`. public var machineValue: String { time.map { "\(date)T\($0)" } ?? date } public var displayValue: String { time.map { "\(date) \($0)" } ?? date } }