Sources/OrgPresentation/ReadingText.swift
100 lines · 5688 bytes
1import Foundation
2import OrgCore
3
4/// A range of a file to read rather than edit: its style runs as segments, with the brackets
5/// and targets of described links hidden (`org-link-descriptive`) and folded drawers shown as
6/// their first line and `…`, as the editor shows them.
7public enum ReadingText {
8 public struct Segment: Sendable, Equatable {
9 public var text: String
10 public var roles: Set<StyleRole>
11 /// The link this text belongs to.
12 public var link: OrgLink?
13 /// Where the text starts in the file.
14 public var offset: Int
15 /// A pretty sub/superscript's body: true raised, false lowered.
16 public var superscript: Bool?
17 }
18
19 /// How the text shows, as the editor's display settings make it.
20 public struct Display: Sendable {
21 /// Link brackets and targets, and emphasis markers, shown; no pretty entities.
22 public var showMarkup = false
23 /// `org-hide-emphasis-markers`, `org-pretty-entities` and its braces.
24 public var options = EditingOptions.org
25 /// Colors code in src blocks.
26 public var highlighter: (any CodeHighlighter)?
27
28 public init(showMarkup: Bool = false, options: EditingOptions = .org, highlighter: (any CodeHighlighter)? = nil) {
29 self.showMarkup = showMarkup
30 self.options = options
31 self.highlighter = highlighter
32 }
33 }
34
35 /// `folded` are drawers' and blocks' hidden ranges (`Wrapper.hidden`).
36 public static func segments(_ tree: OrgTree, in range: Range<Int>, folded: [Range<Int>] = [], display: Display = Display()) -> [Segment] {
37 let text = tree.text as NSString
38 let runs = Presentation.styles(tree, in: range, highlighter: display.highlighter)
39 // A fold hides from the end of the opening line through the closing line, so the
40 // marker sits on the opening line.
41 let folds = folded.compactMap { r -> Range<Int>? in
42 guard r.lowerBound > range.lowerBound, r.lowerBound <= range.upperBound else { return nil }
43 let end = r.upperBound > r.lowerBound && r.upperBound <= text.length && text.character(at: r.upperBound - 1) == 10 ? r.upperBound - 1 : r.upperBound
44 return (r.lowerBound - 1)..<min(end, range.upperBound)
45 }
46 var hidden = runs.filter { !display.showMarkup && Presentation.hidesWithMarkup($0.role, options: display.options) }.map(\.range) + folds
47 // `org-pretty-entities`: an entity's last character shows its character, the rest hidden.
48 var replaced: [Int: String] = [:]
49 var scripts: [(range: Range<Int>, superscript: Bool)] = []
50 if !display.showMarkup, display.options.prettyEntities {
51 let blocks = Presentation.shadedBlocks(tree)
52 let pretty = Presentation.prettyRuns(
53 text, in: range, revealedLine: nil, inBlock: { offset in blocks.contains { $0.contains(offset) } },
54 scriptExclusions: runs.filter { Presentation.excludesScripts($0.role) }.map(\.range),
55 subSuperscriptsNeedBraces: display.options.subSuperscriptsNeedBraces
56 )
57 for (lineStart, run) in pretty {
58 switch run {
59 case .entity(let r, let shown):
60 let last = lineStart + r.upperBound - 1
61 hidden.append((lineStart + r.lowerBound)..<last)
62 replaced[last] = shown
63 case .script(let markers, let body, let superscript):
64 hidden += markers.map { (lineStart + $0.lowerBound)..<(lineStart + $0.upperBound) }
65 scripts.append(((lineStart + body.lowerBound)..<(lineStart + body.upperBound), superscript))
66 }
67 }
68 }
69 var bounds: Set<Int> = [range.lowerBound, range.upperBound]
70 let replacedRanges = replaced.keys.map { $0..<($0 + 1) }
71 for r in runs.map(\.range) + hidden + replacedRanges + scripts.map(\.range) {
72 bounds.insert(max(range.lowerBound, min(range.upperBound, r.lowerBound)))
73 bounds.insert(max(range.lowerBound, min(range.upperBound, r.upperBound)))
74 }
75 let points = bounds.sorted()
76 var segments: [Segment] = []
77 func append(_ segment: Segment) {
78 if var last = segments.last, last.roles == segment.roles, last.link == segment.link, last.superscript == segment.superscript,
79 last.offset + (last.text as NSString).length == segment.offset {
80 last.text += segment.text
81 segments[segments.count - 1] = last
82 } else {
83 segments.append(segment)
84 }
85 }
86 for (a, b) in zip(points, points.dropFirst()) where a < b {
87 if folds.contains(where: { $0.lowerBound == a }) {
88 segments.append(Segment(text: "…", roles: [.metadata], link: nil, offset: a))
89 }
90 if hidden.contains(where: { $0.lowerBound <= a && b <= $0.upperBound }) { continue }
91 let roles = Set(runs.filter { $0.range.lowerBound <= a && b <= $0.range.upperBound }.map(\.role))
92 let link = roles.contains(.link) ? Links.link(at: a, text: tree.text, tree: tree)?.link : nil
93 let superscript = scripts.first { $0.range.lowerBound <= a && b <= $0.range.upperBound }?.superscript
94 let shown = replaced[a] ?? text.substring(with: NSRange(location: a, length: b - a))
95 let segment = Segment(text: shown, roles: roles, link: link, offset: a, superscript: superscript)
96 if replaced[a] != nil { segments.append(segment) } else { append(segment) }
97 }
98 return segments
99 }
100}