krz/orgstar

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

Sources/OrgPresentation/SpellCheck.swift

322a87d47526d505d903e05d14e65a5464e1468f
orgstar/Sources/OrgPresentation/SpellCheck.swift history · blame · raw

44 lines · 2320 bytes

 1import Foundation
 2import OrgCore
 3
 4/// Where spell checking stays off: the faces spell-fu skips in Org (code, links, dates, tags,
 5/// keywords, meta lines, properties, LaTeX, src and example blocks).
 6public enum SpellCheck {
 7    public static func skipped(_ tree: OrgTree) -> [Range<Int>] {
 8        let ns = tree.text as NSString
 9        func line(_ p: Int) -> Range<Int> {
10            let r = ns.lineRange(for: NSRange(location: p, length: 0))
11            return r.location..<(r.location + r.length)
12        }
13        let whole: Set<SyntaxKind> = [.code, .verbatim, .link, .timestamp, .footnoteReference, .statisticsCookie, .target, .radioTarget,
14                                      .macro, .inlineSourceBlock, .latexFragment, .entity, .latexEnvironment, .keyword, .affiliatedKeyword,
15                                      .planning, .clock, .propertyDrawer, .fixedWidth, .tableFormula, .horizontalRule,
16                                      .citation, .exportSnippet, .inlineBabelCall, .diarySexp]
17        let tokens: Set<SyntaxKind> = [.stars, .todoKeyword, .priority, .tags, .bullet, .checkbox, .counter]
18        var ranges: [Range<Int>] = []
19        for node in tree.root.descendants() {
20            if whole.contains(node.kind) {
21                ranges.append(node.range)
22                continue
23            }
24            switch node.kind {
25            case .block, .dynamicBlock, .drawer:
26                let first = line(node.range.lowerBound)
27                let name = ns.substring(with: NSRange(location: first.lowerBound, length: first.count)).trimmingCharacters(in: .whitespaces).lowercased()
28                if node.kind == .block, ["src", "example", "export", "comment"].contains(where: { name.hasPrefix("#+begin_\($0)") }) {
29                    ranges.append(node.range)
30                } else {
31                    ranges.append(first)
32                    var end = node.range.upperBound
33                    while end > node.range.lowerBound, ns.character(at: end - 1) == 10 { end -= 1 }
34                    ranges.append(line(max(node.range.lowerBound, end - 1)))
35                }
36            case .heading, .item:
37                ranges += node.tokens.filter { tokens.contains($0.kind) }.map(\.range)
38            default:
39                break
40            }
41        }
42        return ranges
43    }
44}