import Foundation import Testing @testable import OrgSwift /// Tests for the element tree: `source → OrgDocument → {HTML, AttributedString}`. /// /// The parse tests assert the tree carries structure faithfully. The renderer tests assert /// the point of the split — that a second output format is a walk over the same tree rather /// than a second parser. Corpus conformance is gated separately, in `ConformanceTests`. struct ASTParseTests { @Test func headingCarriesTodoPriorityAndTags() { let doc = OrgParser.parse("* TODO [#A] Write the parser :work:rust:") guard case .heading(let heading) = doc.elements.first else { Issue.record("expected a heading"); return } #expect(heading.level == 1) #expect(heading.todo == "TODO") #expect(heading.priority == "A") #expect(heading.tags == ["work", "rust"]) #expect(OrgParser.plain(heading.title) == "Write the parser") } @Test func emphasisNestsRatherThanFlattening() { // The point of a tree: bold containing italic is structure, not a markup string. let objects = OrgParser.parseInline("*bold /inner/ rest*") guard case .bold(let children) = objects.first else { Issue.record("expected bold"); return } #expect(children.contains { if case .italic = $0 { return true } else { return false } }) } @Test func documentKeywordsAreMetadataNotContent() { let doc = OrgParser.parse("#+TITLE: My Doc\n#+AUTHOR: Someone\n\nBody.") #expect(doc.keyword("title") == "My Doc") #expect(doc.keyword("author") == "Someone") // Metadata does not appear as a body element. #expect(doc.elements.count == 1) guard case .paragraph = doc.elements.first else { Issue.record("expected a single paragraph"); return } } @Test func nestedListsBecomeNestedItems() { let doc = OrgParser.parse(""" - outer - inner - deepest - second """) guard case .list(let list) = doc.elements.first else { Issue.record("expected a list"); return } #expect(list.items.count == 2) let inner = list.items[0].sublist #expect(inner != nil) #expect(inner?.items.first?.sublist?.items.count == 1) } @Test func tableKeepsRuleRowAndAlignments() { let doc = OrgParser.parse(""" | Name | Score | |:------+------:| | alpha | 10 | """) guard case .table(let table) = doc.elements.first else { Issue.record("expected a table"); return } #expect(table.rows.count == 3) #expect(table.headerRowCount == 1) #expect(table.alignments == [.left, .right]) if case .rule = table.rows[1] {} else { Issue.record("row 1 should be the rule") } } @Test func timestampsAndLinksBecomeTypedObjects() { let objects = OrgParser.parseInline("due <2024-01-15 Mon 10:30> see [[id:abc][the thing]]") let hasTimestamp = objects.contains { if case .timestamp(let stamp) = $0 { return stamp.machineValue == "2024-01-15T10:30" } return false } #expect(hasTimestamp) let hasIDLink = objects.contains { if case .link(let link) = $0, case .id(let identifier) = link.target { return identifier == "abc" } return false } #expect(hasIDLink) } } struct ASTRendererTests { /// The payoff: one parse, two output formats, neither re-deriving the other's work. @Test func oneParseFeedsTwoRenderers() { let document = OrgParser.parse("A *bold* claim with ~code~ and a [[https://example.com][link]].") let html = OrgHTMLTreeRenderer().render(document) #expect(html.contains("bold")) #expect(html.contains("code")) #expect(html.contains(#"link"#)) let attributed = OrgAttributedStringRenderer().inline({ if case .paragraph(let objects) = document.elements[0] { return objects } return [] }()) // Same content, native representation: no markup, real attributes. let plain = String(attributed.characters) #expect(plain == "A bold claim with code and a link.") #expect(!plain.contains("<")) let boldRun = attributed.runs.first { $0.inlinePresentationIntent == .stronglyEmphasized } #expect(boldRun != nil) let codeRun = attributed.runs.first { $0.inlinePresentationIntent == .code } #expect(codeRun != nil) let linkRun = attributed.runs.first { $0.link != nil } #expect(linkRun?.link?.absoluteString == "https://example.com") } @Test func attributedStringCarriesRolesForNonStandardIntents() { let objects = OrgParser.parseInline("x^2 and <2024-01-15 Mon>") let attributed = OrgAttributedStringRenderer().inline(objects) let roles: [OrgRole] = attributed.runs.compactMap { $0[OrgRoleAttribute.self] } #expect(roles.contains(.superscript)) #expect(roles.contains(.timestamp)) } @Test func consecutiveListsOfDifferentKindsStaySeparate() { // An ordered list followed by a bullet list is two lists, not one with mixed items. let doc = OrgParser.parse(""" 1. first 2. second - [ ] todo - [X] done """) let lists = doc.elements.compactMap { element -> OrgList? in if case .list(let list) = element { return list } else { return nil } } #expect(lists.count == 2) #expect(lists.first?.kind == .ordered) #expect(lists.last?.kind == .unordered) #expect(lists.last?.items.first?.checkbox == .off) let html = OrgHTMLTreeRenderer().render(doc) #expect(html.contains("")) #expect(html.contains("