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
8enum DocumentInputRules {
9 struct Plan {
10 let kind: String
11 let level: Int
12 let start: Int
13 let paragraphLocation: Int
14 let removeLength: Int
15 let prefix: String
16 let priorBlockType: String
17 let priorAttrs: [String: String]
18 let priorParagraphStyle: NSParagraphStyle
19 }
20 /// Matches fixtures/autoformat.mjs against at most ProseMirror's 500-character
21 /// input-rule context. This describes a transform; it never changes storage.
22 static func plan(in attributed: NSAttributedString, range: NSRange, replacement: String) -> Plan? {
23 guard range.length == 0, range.location >= 0, range.location <= attributed.length,
24 replacement.unicodeScalars.count == 1,
25 let scalar = replacement.unicodeScalars.first,
26 CharacterSet.whitespaces.contains(scalar),
27 !CharacterSet.newlines.contains(scalar) else { return nil }
28 let source = attributed.string as NSString
29 var beginning = range.location
30 while beginning > 0 && source.character(at: beginning - 1) != 10 {
31 guard range.location - beginning < 500 else { return nil }
32 beginning -= 1
33 }
34 let existing = source.substring(with: NSRange(location: beginning, length: range.location - beginning))
35 guard !existing.contains("\u{2028}"), !existing.contains("\r") else { return nil }
36 let prefix = existing + replacement
37 let attributes = beginning < attributed.length ? attributed.attributes(at: beginning, effectiveRange: nil) : [:]
38 let priorType = attributes[.docsBlockType] as? String ?? "paragraph"
39 guard priorType != "code_block" else { return nil }
40 var kind: String
41 var level = 0
42 if prefix.range(of: #"^\s*[-+*]\s\z"#, options: .regularExpression) != nil { kind = "bullet_list" }
43 else if prefix.range(of: #"^[0-9]+\.\s\z"#, options: .regularExpression) != nil { kind = "ordered_list" }
44 else if prefix.range(of: #"^\s*>\s\z"#, options: .regularExpression) != nil { kind = "blockquote" }
45 else if prefix.range(of: #"^#{1,3}\s\z"#, options: .regularExpression) != nil {
46 kind = "heading"
47 level = existing.count
48 } else { return nil }
49 let priorAttrs = (attributes[.docsBlockAttributes] as? Data).flatMap {
50 try? JSONDecoder().decode([String: String].self, from: $0)
51 } ?? [:]
52 let style = (attributes[.paragraphStyle] as? NSParagraphStyle)?.copy() as? NSParagraphStyle ?? NSParagraphStyle.default
53 // The web input rule intentionally starts at 1 regardless of typed digits.
54 return Plan(kind: kind, level: level, start: 1, paragraphLocation: beginning,
55 removeLength: range.location - beginning, prefix: prefix,
56 priorBlockType: priorType, priorAttrs: priorAttrs, priorParagraphStyle: style)
57 }
58}
59