| 1 | import Foundation |
| 2 | |
| 3 | // The format-as-you-type brain (issue #890) — the native twin of the input |
| 4 | // rules in web/static/composer-pm.js. Pure string-in, decision-out, so the |
| 5 | // regex semantics are headless-testable; the editors (ComposerField / |
| 6 | // MacComposerField) own WHEN to consult it (single typed characters only, |
| 7 | // never during IME composition, never inside code) and how to apply the edit. |
| 8 | // |
| 9 | // The regex literals are transliterated from composer-pm.js and drift-gated: |
| 10 | // ComposerRulesTests greps them out of the web file (the ComposerTypeaheadTests |
| 11 | // pattern), so a web-side rule change fails the native build until mirrored. |
| 12 | |
| 13 | public enum RuleMark: Equatable { |
| 14 | case bold, italic, code, strike |
| 15 | } |
| 16 | |
| 17 | public struct InlineCompletion: Equatable { |
| 18 | public let replaceRange: Range<Int> // UTF-16 offsets, relative to the given line |
| 19 | public let content: String |
| 20 | public let mark: RuleMark |
| 21 | } |
| 22 | |
| 23 | // Order matters: strong before em, so the ** closer never half-fires * (the |
| 24 | // composer-pm.js comment, kept). Underscores italicize too — never intraword |
| 25 | // (the lookbehind keeps snake_case whole). Block conversion was deliberately |
| 26 | // removed (composers stick to the inline basics; blocks stay render-only). |
| 27 | private let inlineRules: [(NSRegularExpression, RuleMark)] = [ |
| 28 | (try! NSRegularExpression(pattern: "\\*\\*([^*\\s](?:[^*]*[^*\\s])?)\\*\\*$"), .bold), |
| 29 | (try! NSRegularExpression(pattern: "(?<!\\*)\\*([^*\\s](?:[^*]*[^*\\s])?)\\*$"), .italic), |
| 30 | (try! NSRegularExpression(pattern: "(?<![\\w_])_([^_\\s](?:[^_]*[^_\\s])?)_$"), .italic), |
| 31 | (try! NSRegularExpression(pattern: "`([^`\\s](?:[^`]*[^`\\s])?)`$"), .code), |
| 32 | (try! NSRegularExpression(pattern: "(?<!~)~([^~\\s](?:[^~]*[^~\\s])?)~$"), .strike), |
| 33 | ] |
| 34 | |
| 35 | // The text from the start of the caret's line up to the caret. Returns the |
| 36 | // completion to apply (replace the marker span with styled content) or nil. |
| 37 | public func inlineCompletion(lineUpToCaret line: String) -> InlineCompletion? { |
| 38 | let ns = line as NSString |
| 39 | let full = NSRange(location: 0, length: ns.length) |
| 40 | for (re, mark) in inlineRules { |
| 41 | guard let m = re.firstMatch(in: line, range: full), m.range.location != NSNotFound else { continue } |
| 42 | let content = ns.substring(with: m.range(at: 1)) |
| 43 | return InlineCompletion(replaceRange: m.range.location..<(m.range.location + m.range.length), content: content, mark: mark) |
| 44 | } |
| 45 | return nil |
| 46 | } |
| 47 | |
| 48 | |
| 49 | |