krz/org-swift

A dependency-free Swift library that renders org-mode to sanitized HTML. html library org-mode swift

Commit ed82eb8293

ed82eb82931491148426514778f20093ec885475

parent: d91c9b5160

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-30 05:08 UTC

Parser: a line starting with # that is not a comment is prose

startsNewConstruct treated any #-prefixed line as a new construct, so a
line such as "#12 fixed this", which no branch above claims, ended the
paragraph before it was consumed and parse looped forever. Only #+
keywords and comments (# or "# ") start a construct now, and a
paragraph always takes its first line.

Layout: unified · split

Sources/OrgSwift/AST/OrgParser.swift +6 −2
@@ -138,7 +138,10 @@ public enum OrgParser {
138138 while index < lines.count {
139139 let candidate = lines[index]
140140 let candidateTrimmed = candidate.trimmingCharacters(in: .whitespaces)
141 if candidateTrimmed.isEmpty || startsNewConstruct(candidateTrimmed, raw: candidate) { break }
141 // The first line is always taken: nothing above claimed it, and
142 // leaving it would parse the same line forever.
143 if !paragraph.isEmpty,
144 candidateTrimmed.isEmpty || startsNewConstruct(candidateTrimmed, raw: candidate) { break }
142145 paragraph.append(candidateTrimmed)
143146 index += 1
144147 }
@@ -153,7 +156,8 @@ public enum OrgParser {
153156
154157 /// Would this line begin a construct other than the paragraph currently being consumed?
155158 private static func startsNewConstruct(_ trimmed: String, raw: String) -> Bool {
156 if trimmed.hasPrefix("#+") || trimmed.hasPrefix("#") { return true }
159 // A keyword or a comment; `#12 fixed this` is prose.
160 if trimmed.hasPrefix("#+") || trimmed == "#" || trimmed.hasPrefix("# ") { return true }
157161 if trimmed.firstMatch(of: /^\*{1,6}\s+/) != nil { return true }
158162 if isOrgHorizontalRule(trimmed) { return true }
159163 if isTableLine(trimmed) { return true }
Tests/OrgSwiftTests/OrgTreeTests.swift +22
@@ -9,6 +9,28 @@ import Testing
99/// than a second parser. Corpus conformance is gated separately, in `ConformanceTests`.
1010struct ASTParseTests {
1111
12 @Test(.timeLimit(.minutes(1)))
13 func aHashLineThatIsNotACommentIsAParagraph() {
14 let doc = OrgParser.parse("#12 fixed this\nsee #1")
15 guard case .paragraph(let objects) = doc.elements.first else {
16 Issue.record("expected a paragraph, got \(doc.elements)"); return
17 }
18 #expect(doc.elements.count == 1)
19 #expect(!objects.isEmpty)
20 }
21
22 @Test(.timeLimit(.minutes(1)))
23 func aHashLineEndsNoParagraph() {
24 let doc = OrgParser.parse("see\n#12 for context")
25 #expect(doc.elements.count == 1)
26 }
27
28 @Test(.timeLimit(.minutes(1)))
29 func aCommentStillEndsAParagraph() {
30 let doc = OrgParser.parse("one\n# a comment\ntwo")
31 #expect(doc.elements.count == 2)
32 }
33
1234 @Test
1335 func headingCarriesTodoPriorityAndTags() {
1436 let doc = OrgParser.parse("* TODO [#A] Write the parser :work:rust:")