Sources/OrgPresentation/ReadingText.swift
55 lines · 2974 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 }
16
17 /// `folded` are drawers' and blocks' hidden ranges (`Wrapper.hidden`).
18 public static func segments(_ tree: OrgTree, in range: Range<Int>, folded: [Range<Int>] = []) -> [Segment] {
19 let text = tree.text as NSString
20 let runs = Presentation.styles(tree, in: range)
21 // A fold hides from the end of the opening line through the closing line, so the
22 // marker sits on the opening line.
23 let folds = folded.compactMap { r -> Range<Int>? in
24 guard r.lowerBound > range.lowerBound, r.lowerBound <= range.upperBound else { return nil }
25 let end = r.upperBound > r.lowerBound && r.upperBound <= text.length && text.character(at: r.upperBound - 1) == 10 ? r.upperBound - 1 : r.upperBound
26 return (r.lowerBound - 1)..<min(end, range.upperBound)
27 }
28 let hidden = runs.filter { $0.role == .linkMarkup }.map(\.range) + folds
29 var bounds: Set<Int> = [range.lowerBound, range.upperBound]
30 for r in runs.map(\.range) + hidden {
31 bounds.insert(max(range.lowerBound, min(range.upperBound, r.lowerBound)))
32 bounds.insert(max(range.lowerBound, min(range.upperBound, r.upperBound)))
33 }
34 let points = bounds.sorted()
35 var segments: [Segment] = []
36 func append(_ segment: Segment) {
37 if var last = segments.last, last.roles == segment.roles, last.link == segment.link, last.offset + (last.text as NSString).length == segment.offset {
38 last.text += segment.text
39 segments[segments.count - 1] = last
40 } else {
41 segments.append(segment)
42 }
43 }
44 for (a, b) in zip(points, points.dropFirst()) where a < b {
45 if folds.contains(where: { $0.lowerBound == a }) {
46 segments.append(Segment(text: "…", roles: [.metadata], link: nil, offset: a))
47 }
48 if hidden.contains(where: { $0.lowerBound <= a && b <= $0.upperBound }) { continue }
49 let roles = Set(runs.filter { $0.range.lowerBound <= a && b <= $0.range.upperBound }.map(\.role))
50 let link = roles.contains(.link) ? Links.link(at: a, text: tree.text, tree: tree)?.link : nil
51 append(Segment(text: text.substring(with: NSRange(location: a, length: b - a)), roles: roles, link: link, offset: a))
52 }
53 return segments
54 }
55}