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 | 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 | 149 | private static func isWordCharacter(_ c: Character) -> Bool { |
| 140 | 150 | c.isLetter || c.isNumber |
| 141 | 151 | } |
| @@ -154,7 +164,7 @@ extension OrgParser { | ||
| 154 | 164 | if chars[j] == marker, let last = body.last, !last.isWhitespace { |
| 155 | 165 | // The closer must end the run or be followed by space/punctuation. |
| 156 | 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 | 168 | return (body, j + 1) |
| 159 | 169 | } |
| 160 | 170 | } |
Tests/OrgSwiftTests/OrgTreeTests.swift +28
| @@ -344,6 +344,34 @@ struct OrgRenderOptionsTests { | ||
| 344 | 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 | 375 | @Test |
| 348 | 376 | func rejectsUnsafeSchemes() { |
| 349 | 377 | let html = OrgRenderer.renderToHTML("[[javascript:alert(1)][click]]") |