krz/org-swift

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

Commit 6e2ef80a16

6e2ef80a16a9eb61bff7f9928c230f4c5dff8240

parent: 1873fef702

Unsigned

cmc <hello@cleberg.net> · 2026-08-31 21:44 UTC

Match orgo's emphasis boundary characters

The characters allowed before an opening marker and after a closing one
were a narrower set than org's, so `=verbatim=-adjacent` did not close and
`-=code=` did not open. Both sets now match orgo, which is what the
conformance corpus is generated from: `-({'"` before, and
`-.,;:!?)}["'` after.

`]` leaves the post set, where it was never part of org's. That is safe
because a link's description is parsed as its own run, so a closing `]`
never follows a marker — asserted rather than assumed.

Closes #1

Layout: unified · split

Sources/OrgSwift/AST/OrgInlineParser.swift +13 −3
@@ -131,11 +131,21 @@ extension OrgParser {
131 } 131 }
132 } 132 }
133 133
134 /// org requires the opening marker to follow whitespace or start the run. 134 /// The character before an opening marker. org allows the start of the run,
135 /// whitespace, or one of `-({'"` — the same set orgo uses, which is what the
136 /// conformance corpus is generated from.
135 private static func boundaryBefore(_ chars: [Character], _ i: Int) -> Bool { 137 private static func boundaryBefore(_ chars: [Character], _ i: Int) -> Bool {
136 i == 0 || chars[i - 1].isWhitespace || "([{'\"".contains(chars[i - 1]) 138 i == 0 || chars[i - 1].isWhitespace || preChars.contains(chars[i - 1])
137 } 139 }
138 140
141 private static let preChars: Set<Character> = ["-", "(", "{", "'", "\""]
142
143 /// The character after a closing marker, likewise from orgo. Note `[` is in the
144 /// set and `]` is not, which looks backwards until you remember a link's
145 /// description is parsed as its own run, so a `]` never follows a closer.
146 private static let postChars: Set<Character> =
147 ["-", ".", ",", ";", ":", "!", "?", ")", "}", "[", "\"", "'"]
148
139 private static func isWordCharacter(_ c: Character) -> Bool { 149 private static func isWordCharacter(_ c: Character) -> Bool {
140 c.isLetter || c.isNumber 150 c.isLetter || c.isNumber
141 } 151 }
@@ -154,7 +164,7 @@ extension OrgParser {
154 if chars[j] == marker, let last = body.last, !last.isWhitespace { 164 if chars[j] == marker, let last = body.last, !last.isWhitespace {
155 // The closer must end the run or be followed by space/punctuation. 165 // The closer must end the run or be followed by space/punctuation.
156 let after = j + 1 < chars.count ? chars[j + 1] : " " 166 let after = j + 1 < chars.count ? chars[j + 1] : " "
157 if after.isWhitespace || ".,;:!?)]}'\"".contains(after) || j + 1 == chars.count { 167 if after.isWhitespace || postChars.contains(after) {
158 return (body, j + 1) 168 return (body, j + 1)
159 } 169 }
160 } 170 }
Tests/OrgSwiftTests/OrgTreeTests.swift +28
@@ -344,6 +344,34 @@ struct OrgRenderOptionsTests {
344 #expect(bold.contains(.bold([.text("two words")]))) 344 #expect(bold.contains(.bold([.text("two words")])))
345 } 345 }
346 346
347 /// The characters org allows around emphasis markers, matched to orgo — which is
348 /// what the conformance corpus is generated from. The set was narrower here, so a
349 /// closer against a dash or a bracket did not close.
350 @Test
351 func emphasisBoundaryCharactersMatchOrgo() {
352 func verbatims(_ text: String) -> [String] {
353 OrgParser.parseInline(text).compactMap {
354 if case .verbatim(let t) = $0 { return t } else { return nil }
355 }
356 }
357 // Newly closing: a dash or an opening bracket after the closer.
358 #expect(verbatims("=verbatim=-adjacent") == ["verbatim"])
359 #expect(verbatims("see =code=[1]") == ["code"])
360 // Newly opening: a dash before the opener.
361 #expect(verbatims("-=code= wrapped") == ["code"])
362
363 // `]` is not a post character in org, so this does not close.
364 #expect(verbatims("=code=] trailing").isEmpty)
365
366 // Which is safe because a link's description is parsed as its own run, so a
367 // closing `]` never actually follows the marker.
368 let link = OrgParser.parseInline("[[https://example.com][=code=]]")
369 guard case .link(let l) = link.first else {
370 Issue.record("expected a link"); return
371 }
372 #expect(l.description ?? [] == [.verbatim("code")])
373 }
374
347 @Test 375 @Test
348 func rejectsUnsafeSchemes() { 376 func rejectsUnsafeSchemes() {
349 let html = OrgRenderer.renderToHTML("[[javascript:alert(1)][click]]") 377 let html = OrgRenderer.renderToHTML("[[javascript:alert(1)][click]]")