krz/orgstar

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

Sources/OrgPresentation/ReadingText.swift

55 lines · 2974 bytes

4 symbols in this file
 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}