krz/org-swift

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

Commit 77bb69d3d0

77bb69d3d00d14411ae7d10a935711590a52114f

parent: ca2a0de207

Unsigned

cmc <hello@cleberg.net> · 2026-08-31 18:30 UTC

Apply org's border rule to emphasis

An emphasis body may not begin or end with whitespace. Only the post-closer
character was checked, so `=ssh.mode = "system"=` closed at the middle `=`
and rendered as two verbatim spans with the quoted value loose between
them, in both renderers. Corpus conformance is unchanged at 11/12.

Layout: unified · split

Sources/OrgSwift/AST/OrgInlineParser.swift +8 −2
@@ -142,13 +142,19 @@ extension OrgParser {
142 142
143 private static func scanEmphasis(_ chars: [Character], from start: Int, marker: Character) 143 private static func scanEmphasis(_ chars: [Character], from start: Int, marker: Character)
144 -> (body: String, next: Int)? { 144 -> (body: String, next: Int)? {
145 // org's border rule: the body may not begin with whitespace, so an opener followed by
146 // a space is not an opener at all.
147 guard start + 1 < chars.count, !chars[start + 1].isWhitespace else { return nil }
148
145 var j = start + 1 149 var j = start + 1
146 var body = "" 150 var body = ""
147 while j < chars.count { 151 while j < chars.count {
148 if chars[j] == marker { 152 // The body may not end with whitespace either — which is what makes the closer of
153 // `=ssh.mode = "system"=` the last `=` and not the middle one.
154 if chars[j] == marker, let last = body.last, !last.isWhitespace {
149 // The closer must end the run or be followed by space/punctuation. 155 // The closer must end the run or be followed by space/punctuation.
150 let after = j + 1 < chars.count ? chars[j + 1] : " " 156 let after = j + 1 < chars.count ? chars[j + 1] : " "
151 if !body.isEmpty, after.isWhitespace || ".,;:!?)]}'\"".contains(after) || j + 1 == chars.count { 157 if after.isWhitespace || ".,;:!?)]}'\"".contains(after) || j + 1 == chars.count {
152 return (body, j + 1) 158 return (body, j + 1)
153 } 159 }
154 } 160 }
Tests/OrgSwiftTests/OrgTreeTests.swift +21
@@ -323,6 +323,27 @@ struct OrgRenderOptionsTests {
323 #expect(without.contains("<p>Body.</p>")) 323 #expect(without.contains("<p>Body.</p>"))
324 } 324 }
325 325
326 /// org's border rule: an emphasis body may not begin or end with whitespace. Without the
327 /// trailing half, `=ssh.mode = "system"=` closed at the middle `=` and rendered as two
328 /// verbatim spans with the quoted value loose between them.
329 @Test
330 func emphasisBodyCannotBeginOrEndWithWhitespace() {
331 let objects = OrgParser.parseInline(#"=ssh.mode = "system"= to run"#)
332 let verbatims = objects.compactMap { object -> String? in
333 if case .verbatim(let text) = object { return text } else { return nil }
334 }
335 #expect(verbatims == [#"ssh.mode = "system""#])
336
337 // A leading space means the marker never opens.
338 #expect(OrgParser.parseInline("= not verbatim=") == [.text("= not verbatim=")])
339
340 // The ordinary cases still work.
341 #expect(OrgParser.parseInline("=plain=") == [.verbatim("plain")])
342 let bold = OrgParser.parseInline("*bold* and *two words*")
343 #expect(bold.contains(.bold([.text("bold")])))
344 #expect(bold.contains(.bold([.text("two words")])))
345 }
346
326 @Test 347 @Test
327 func rejectsUnsafeSchemes() { 348 func rejectsUnsafeSchemes() {
328 let html = OrgRenderer.renderToHTML("[[javascript:alert(1)][click]]") 349 let html = OrgRenderer.renderToHTML("[[javascript:alert(1)][click]]")