Sources/OrgPresentation/SpellCheck.swift
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}