oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2#if os(macOS)
3import AppKit
4#else
5import UIKit
6#endif
7
8@MainActor extension DocsTextView {
9 private var ruleContent: NSAttributedString {
10 #if os(macOS)
11 attributedString()
12 #else
13 attributedText
14 #endif
15 }
16 var ruleCaret: Int { ruleSelection.location }
17 private var ruleSelection: NSRange {
18 #if os(macOS)
19 selectedRange()
20 #else
21 selectedRange
22 #endif
23 }
24 private func replaceRule(_ range: NSRange, with value: NSAttributedString) {
25 #if os(macOS)
26 textStorage?.replaceCharacters(in: range, with: value)
27 setSelectedRange(NSRange(location: range.location + value.length, length: 0))
28 didChangeText()
29 #else
30 textStorage.replaceCharacters(in: range, with: value)
31 selectedRange = NSRange(location: range.location + value.length, length: 0)
32 delegate?.textViewDidChange?(self)
33 #endif
34 }
35 func interceptInput(_ text: String, range: NSRange) -> Bool {
36 guard isEditable else { return true }
37 if text.isEmpty, lastInputRule != nil, revertInputRule() { return true }
38 lastInputRule = nil
39 if let plan = DocumentInputRules.plan(in: ruleContent, range: range, replacement: text) {
40 let command: [String: JSONValue] = ["op": .string("display_autoformat"), "at": .number(Double(plan.paragraphLocation)), "length": .number(Double(plan.removeLength)), "kind": .string(plan.kind), "level": .number(Double(plan.level))]
41 guard ruleCommand?(command) == true else { return true }
42 replaceRule(NSRange(location: plan.paragraphLocation, length: plan.removeLength), with: NSAttributedString(string: ""))
43 let style = (plan.priorParagraphStyle.mutableCopy() as? NSMutableParagraphStyle) ?? NSMutableParagraphStyle()
44 if plan.kind == "bullet_list" || plan.kind == "ordered_list" {
45 style.textLists = [NSTextList(markerFormat: plan.kind == "bullet_list" ? .disc : .decimal, options: 0)]
46 }
47 let paragraph = (ruleContent.string as NSString).paragraphRange(for: NSRange(location: min(plan.paragraphLocation, max(0, ruleContent.length - 1)), length: 0))
48 let attributes: [NSAttributedString.Key: Any] = [.paragraphStyle: style, .docsBlockType: plan.kind == "heading" ? "heading" : "paragraph", .docsHeading: plan.level]
49 #if os(macOS)
50 textStorage?.addAttributes(attributes, range: paragraph)
51 #else
52 textStorage.addAttributes(attributes, range: paragraph)
53 #endif
54 lastInputRule = plan
55 return true
56 }
57 if text == "\n", range.length == 0, ruleContent.length > 0 {
58 let raw = ruleContent.string as NSString
59 let paragraph = raw.paragraphRange(for: NSRange(location: min(range.location, raw.length - 1), length: 0))
60 let value = raw.substring(with: paragraph)
61 let style = ruleContent.attribute(.paragraphStyle, at: min(range.location, raw.length - 1), effectiveRange: nil) as? NSParagraphStyle
62 if value == "\n", style?.textLists.isEmpty == false {
63 guard ruleCommand?(["op": .string("display_exit_list"), "at": .number(Double(range.location)), "kind": .string("paragraph")]) == true else { return true }
64 let plain = (style?.mutableCopy() as? NSMutableParagraphStyle) ?? NSMutableParagraphStyle()
65 plain.textLists = []; plain.headIndent = 0; plain.firstLineHeadIndent = 0
66 replaceRule(paragraph, with: NSAttributedString(string: "\n", attributes: [.paragraphStyle: plain]))
67 #if os(macOS)
68 setSelectedRange(range)
69 #else
70 selectedRange = range
71 #endif
72 return true
73 }
74 }
75 return false
76 }
77 func revertInputRule() -> Bool {
78 guard !isComposing, isEditable, let plan = lastInputRule,
79 ruleSelection == NSRange(location: plan.paragraphLocation, length: 0) else { lastInputRule = nil; return false }
80 let command: [String: JSONValue] = ["op": .string("display_revert_rule"), "at": .number(Double(plan.paragraphLocation)), "kind": .string(plan.kind), "prior_type": .string(plan.priorBlockType), "prior_attrs": .object(plan.priorAttrs.mapValues(JSONValue.string)), "text": .string(plan.prefix)]
81 guard ruleCommand?(command) == true else { return true }
82 replaceRule(NSRange(location: plan.paragraphLocation, length: 0), with: NSAttributedString(string: plan.prefix, attributes: [.paragraphStyle: plan.priorParagraphStyle, .docsBlockType: plan.priorBlockType]))
83 let paragraph = (ruleContent.string as NSString).paragraphRange(for: NSRange(location: plan.paragraphLocation, length: 0))
84 let restored: [NSAttributedString.Key: Any] = [.paragraphStyle: plan.priorParagraphStyle, .docsBlockType: plan.priorBlockType, .docsBlockAttributes: (try? JSONEncoder().encode(plan.priorAttrs)) ?? Data()]
85 #if os(macOS)
86 textStorage?.addAttributes(restored, range: paragraph)
87 #else
88 textStorage.addAttributes(restored, range: paragraph)
89 #endif
90 lastInputRule = nil
91 return true
92 }
93}
94