Commit 6e2ef80a16
Unsigned
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]]") |