krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Sources/OrgCore/Semantic/PrettyEntities.swift

54 lines · 3145 bytes

3 symbols in this file
 1import Foundation
 2
 3/// What `org-pretty-entities` changes on a line (`org-fontify-entities`, `org-raise-scripts`):
 4/// single-character entities show as that character, and sub/superscripts lose their `_`, `^`
 5/// and braces.
 6public enum PrettyEntities {
 7    public enum Run: Equatable, Sendable {
 8        /// `\alpha` or `\alpha{}`, shown as `display`. Offsets are UTF-16, within the line.
 9        case entity(range: Range<Int>, display: String)
10        /// A sub/superscript: `markers` (`_` or `^`, and braces) are hidden and `body` raised
11        /// or lowered.
12        case script(markers: [Range<Int>], body: Range<Int>, superscript: Bool)
13    }
14
15    static let entity = try! NSRegularExpression(pattern: "\\\\(there4|sup[123]|frac[13][24]|[a-zA-Z]+)($|\\{\\}|[^\\p{L}\\n])")
16    static let scriptsWithBraces = try! NSRegularExpression(pattern: "\\S([_^])(\\{)[^{}\\n]*(\\})")
17    static let scripts = try! NSRegularExpression(pattern: "\\S([_^])(?:(\\{)[^{}\\n]*(\\})|[*]|[-+]?[^-+*!@#$%^_ \\t\\r\\n,:\"?<>~;./{}=()]+)")
18
19    public static func runs(in line: String, subSuperscriptsNeedBraces: Bool) -> [Run] {
20        guard line.contains(where: { $0 == "\\" || $0 == "_" || $0 == "^" }) else { return [] }
21        let ns = line as NSString
22        let all = NSRange(location: 0, length: ns.length)
23        var runs: [Run] = []
24        for match in entity.matches(in: line, range: all) {
25            guard let display = OrgEntities.display[ns.substring(with: match.range(at: 1))], display.count == 1 else { continue }
26            let end = ns.substring(with: match.range(at: 2)) == "{}" ? NSMaxRange(match.range(at: 2)) : NSMaxRange(match.range(at: 1))
27            runs.append(.entity(range: match.range.location..<end, display: display))
28        }
29        // `org-raise-scripts` searches again from the last character of a match that a `_` or
30        // `^` follows, so `a_b^c` raises both.
31        let regex = subSuperscriptsNeedBraces ? scriptsWithBraces : scripts
32        var matches: [NSTextCheckingResult] = []
33        var from = 0
34        while from < ns.length, let match = regex.firstMatch(in: line, range: NSRange(location: from, length: ns.length - from)) {
35            matches.append(match)
36            let end = NSMaxRange(match.range)
37            from = end < ns.length && (ns.character(at: end) == 95 || ns.character(at: end) == 94) ? end - 1 : end
38            if from <= match.range.location { from = match.range.location + 1 }
39        }
40        for match in matches {
41            let markers = (1...3).compactMap { group -> Range<Int>? in
42                let r = match.range(at: group)
43                return r.location == NSNotFound ? nil : r.location..<NSMaxRange(r)
44            }
45            let marker = match.range(at: 1)
46            let hasBraces = match.range(at: 2).location != NSNotFound
47            let body = hasBraces
48                ? NSMaxRange(match.range(at: 2))..<match.range(at: 3).location
49                : NSMaxRange(marker)..<NSMaxRange(match.range)
50            runs.append(.script(markers: markers, body: body, superscript: ns.substring(with: marker) == "^"))
51        }
52        return runs
53    }
54}