Files
AzooKeyKanaKanjiConverter/Sources/KanaKanjiConverterModule/InputManagement/ComposingText.swift

680 lines
33 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// ComposingText.swift
// Keyboard
//
// Created by ensan on 2022/09/21.
// Copyright © 2022 ensan. All rights reserved.
//
import Foundation
import SwiftUtils
/// 3
/// - `input`: `[k, y, o, u, h, a, a, m, e]`
/// - `convertTarget`: ``
/// `
///
/// inputStyle`input` / `delete` / `moveCursor` / `complete`
public struct ComposingText: Sendable {
public init(convertTargetCursorPosition: Int = 0, input: [ComposingText.InputElement] = [], convertTarget: String = "") {
self.convertTargetCursorPosition = convertTargetCursorPosition
self.input = input
self.convertTarget = convertTarget
}
/// 0
public private(set) var convertTargetCursorPosition: Int = 0
/// historydeletemove cursor
public private(set) var input: [InputElement] = []
///
public private(set) var convertTarget: String = ""
///
public struct InputElement: Sendable {
///
public var character: Character
/// ( / )
public var inputStyle: InputStyle
}
///
public var isEmpty: Bool {
self.convertTarget.isEmpty
}
///
public var isAtEndIndex: Bool {
self.convertTarget.count == self.convertTargetCursorPosition
}
///
public var isAtStartIndex: Bool {
0 == self.convertTargetCursorPosition
}
///
public var convertTargetBeforeCursor: some StringProtocol {
self.convertTarget.prefix(self.convertTargetCursorPosition)
}
/// `input`
/// `target`
/// `input``[k, y, o, u]``target``|`
/// `input`
/// `input``[, , u]``|``1`
private mutating func forceGetInputCursorPosition(target: some StringProtocol) -> Int {
debug("ComposingText forceGetInputCursorPosition", self, target)
if target.isEmpty {
return 0
}
// 1
// input: `k, a, n, s, h, a` (roman2kana)
// convetTarget: ` | `
// convertTargetCursorPosition: 3
// target:
//
// 1. character = "k"
// roman2kana = "k"
// count = 1
// 2. character = "a"
// roman2kana = ""
// count = 2
// target.hasPrefix(roman2kana)truelastPrefixIndex = 2, lastPrefix = ""
// 3. character = "n"
// roman2kana = "n"
// count = 3
// 4. character = "s"
// roman2kana = "s"
// count = 4
// 5. character = "h"
// roman2kana = "sh"
// count = 5
// 6. character = "a"
// roman2kana = ""
// count = 6
// roman2kana.hasPrefix(target)true調
// replaceCount6-2 = 4`n, s, h, a`
// input = [k, a]
// count = 2
// roman2kana.count == 4, lastPrefix.count = 13suffix`,,`
// input = [k, a, , , ]
// count = 5
// while
// 1. roman2kana =
// count = 4
// break
// return count = 4
//
// 2
// input: `k, a, n, s, h, a` (roman2kana)
// convetTarget: ` | `
// convertTargetCursorPosition: 2
// target:
//
// 1. character = "k"
// roman2kana = "k"
// count = 1
// 2. character = "a"
// roman2kana = ""
// count = 2
// target.hasPrefix(roman2kana)truelastPrefixIndex = 2, lastPrefix = ""
// 3. character = "n"
// roman2kana = "n"
// count = 3
// 4. character = "s"
// roman2kana = "s"
// count = 4
// roman2kana.hasPrefix(target)true調
// replaceCount4-2 = 2`n, s`
// input = [k, a] ... [h, a]
// count = 2
// roman2kana.count == 3, lastPrefix.count = 12suffix`,s`
// input = [k, a, , s]
// count = 4
// while
// 1. roman2kana =
// count = 3
// break
// return count = 3
//
// 3
// input: `i, t, t, a` (roman2kana)
// convetTarget: ` | `
// convertTargetCursorPosition: 2
// target:
//
// 1. character = "i"
// roman2kana = ""
// count = 1
// target.hasPrefix(roman2kana)truelastPrefixIndex = 1, lastPrefix = ""
// 2. character = "t"
// roman2kana = "t"
// count = 2
// 3. character = "t"
// roman2kana = "t"
// count = 3
// roman2kana.hasPrefix(target)true調
// replaceCount3-1 = 2`t, t`
// input = [i] ... [a]
// count = 1
// roman2kana.count == 3, lastPrefix.count = 12suffix`,t`
// input = [i, , t, a]
// count = 3
// while
// 1. roman2kana =
// count = 2
// break
// return count = 2
var count = 0
var lastPrefixIndex = 0
var lastPrefix = ""
var converting: [ConvertTargetElement] = []
for element in input {
Self.updateConvertTargetElements(currentElements: &converting, newElement: element)
var converted = converting.reduce(into: "") {$0 += $1.string}
count += 1
//
if converted == target {
return count
}
// hasPrefix
// input
// covnertTarget|`[a, k, y, o]`prefix
// lastPrefix=11(suffix)
else if converted.hasPrefix(target) {
let replaceCount = count - lastPrefixIndex
let suffix = converted.suffix(converted.count - lastPrefix.count)
self.input.removeSubrange(count - replaceCount ..< count)
self.input.insert(contentsOf: suffix.map {InputElement(character: $0, inputStyle: CharacterUtils.isRomanLetter($0) ? .roman2kana : .direct)}, at: count - replaceCount)
count -= replaceCount
count += suffix.count
while converted != target {
_ = converted.popLast()
count -= 1
}
break
}
// prefix
else if target.hasPrefix(converted) {
lastPrefixIndex = count
lastPrefix = converted
}
}
return count
}
private func diff(from oldString: some StringProtocol, to newString: String) -> (delete: Int, input: String) {
let common = oldString.commonPrefix(with: newString)
return (oldString.count - common.count, String(newString.dropFirst(common.count)))
}
/// input
/// TODO:
private mutating func updateInput(_ string: String, at inputCursorPosition: Int, inputStyle: InputStyle) {
if inputCursorPosition == 0 {
self.input.insert(contentsOf: string.map {InputElement(character: $0, inputStyle: inputStyle)}, at: inputCursorPosition)
return
}
let prev = self.input[inputCursorPosition - 1]
if inputStyle == .roman2kana && prev.inputStyle == inputStyle, let first = string.first, String(first).onlyRomanAlphabet {
if prev.character == first && !["a", "i", "u", "e", "o", "n"].contains(first) {
self.input[inputCursorPosition - 1] = InputElement(character: "", inputStyle: .direct)
self.input.insert(contentsOf: string.map {InputElement(character: $0, inputStyle: inputStyle)}, at: inputCursorPosition)
return
}
let n_prefix = self.input[0 ..< inputCursorPosition].suffix {$0.character == "n" && $0.inputStyle == .roman2kana}
if n_prefix.count % 2 == 1 && !["n", "a", "i", "u", "e", "o", "y"].contains(first)
&& self.input.dropLast(n_prefix.count).last != .init(character: "x", inputStyle: .roman2kana) {
self.input[inputCursorPosition - 1] = InputElement(character: "", inputStyle: .direct)
self.input.insert(contentsOf: string.map {InputElement(character: $0, inputStyle: inputStyle)}, at: inputCursorPosition)
return
}
}
self.input.insert(contentsOf: string.map {InputElement(character: $0, inputStyle: inputStyle)}, at: inputCursorPosition)
}
///
public mutating func insertAtCursorPosition(_ string: String, inputStyle: InputStyle) {
if string.isEmpty {
return
}
let inputCursorPosition = self.forceGetInputCursorPosition(target: self.convertTarget.prefix(convertTargetCursorPosition))
// input, convertTarget, convertTargetCursorPosition3
// input
self.updateInput(string, at: inputCursorPosition, inputStyle: inputStyle)
let oldConvertTarget = self.convertTarget.prefix(self.convertTargetCursorPosition)
let newConvertTarget = Self.getConvertTarget(for: self.input.prefix(inputCursorPosition + string.count))
let diff = self.diff(from: oldConvertTarget, to: newConvertTarget)
// convertTarget
self.convertTarget.removeFirst(convertTargetCursorPosition)
self.convertTarget.insert(contentsOf: newConvertTarget, at: convertTarget.startIndex)
// convertTargetCursorPosition
self.convertTargetCursorPosition -= diff.delete
self.convertTargetCursorPosition += diff.input.count
}
///
public mutating func deleteForwardFromCursorPosition(count: Int) {
let count = min(convertTarget.count - convertTargetCursorPosition, count)
if count == 0 {
return
}
self.convertTargetCursorPosition += count
self.deleteBackwardFromCursorPosition(count: count)
}
///
/// `sha: |`1`[s, h, a]``[, ]`
public mutating func deleteBackwardFromCursorPosition(count: Int) {
let count = min(convertTargetCursorPosition, count)
if count == 0 {
return
}
// 1
// convertTarget: |
// input: [k, a, n, s, h, a]
// count = 1
// currentPrefix =
//
// targetCursorPosition = forceGetInputCursorPosition() = 4
// input[k, a, , , ]
//
// inputCursorPosition = forceGetInputCursorPosition() = 5
// input[k, a, , , ]
// input
// input = (input.prefix(targetCursorPosition) = [k, a, , ])
// + (input.suffix(input.count - inputCursorPosition) = [])
// = [k, a, , ]
// 2
// convertTarget: |
// input: [k, a, n, s, h, a]
// count = 2
// currentPrefix =
//
// targetCursorPosition = forceGetInputCursorPosition() = 3
// input[k, a, , s, h, a]
//
// inputCursorPosition = forceGetInputCursorPosition() = 6
// input[k, a, , s, h, a]
// input
// input = (input.prefix(targetCursorPosition) = [k, a, ])
// + (input.suffix(input.count - inputCursorPosition) = [])
// = [k, a, ]
//
let currentPrefix = self.convertTargetBeforeCursor
// 2
//
let targetCursorPosition = self.forceGetInputCursorPosition(target: currentPrefix.dropLast(count))
//
let inputCursorPosition = self.forceGetInputCursorPosition(target: currentPrefix)
// input
self.input.removeSubrange(targetCursorPosition ..< inputCursorPosition)
//
self.convertTargetCursorPosition -= count
// convetTarget
self.convertTarget = Self.getConvertTarget(for: self.input)
}
///
/// - parameters:
/// - count: `convertTarget`
/// - returns:
/// - note:
public mutating func moveCursorFromCursorPosition(count: Int) -> Int {
let count = max(min(self.convertTarget.count - self.convertTargetCursorPosition, count), -self.convertTargetCursorPosition)
self.convertTargetCursorPosition += count
return count
}
///
/// - parameters:
/// - correspondingCount: `input`
public mutating func prefixComplete(composingCount: ComposingCount) {
switch composingCount {
case .inputCount(let correspondingCount):
let correspondingCount = min(correspondingCount, self.input.count)
self.input.removeFirst(correspondingCount)
// convetTarget
let newConvertTarget = Self.getConvertTarget(for: self.input)
//
let cursorDelta = self.convertTarget.count - newConvertTarget.count
self.convertTarget = newConvertTarget
self.convertTargetCursorPosition -= cursorDelta
//
if self.convertTargetCursorPosition == 0 {
self.convertTargetCursorPosition = self.convertTarget.count
}
case .surfaceCount(let correspondingCount):
// correspondingCount
//
let prefix = self.convertTarget.prefix(correspondingCount)
let index = self.forceGetInputCursorPosition(target: prefix)
self.input = Array(self.input[index...])
self.convertTarget = String(self.convertTarget.dropFirst(correspondingCount))
self.convertTargetCursorPosition -= correspondingCount
//
if self.convertTargetCursorPosition == 0 {
self.convertTargetCursorPosition = self.convertTarget.count
}
case .composite(let left, let right):
self.prefixComplete(composingCount: left)
self.prefixComplete(composingCount: right)
}
}
/// ComposingText
public func prefixToCursorPosition() -> ComposingText {
var text = self
let index = text.forceGetInputCursorPosition(target: text.convertTarget.prefix(text.convertTargetCursorPosition))
text.input = Array(text.input.prefix(index))
text.convertTarget = String(text.convertTarget.prefix(text.convertTargetCursorPosition))
return text
}
public func inputIndexToSurfaceIndexMap() -> [Int: Int] {
// i2c: input indexconvert target indexmap
// c2i: convert target indexinput indexmap
// 1.
// [k, y, o, u, h, a, i, i, t, e, n, k, i, d, a]
// [, , , , , , , , , ]
// i2c: [0: 0, 3: 2(), 4: 3(), 6: 4(), 7: 5(), 8: 6(), 10: 7(), 13: 9(), 15: 10()]
var map: [Int: (surfaceIndex: Int, surface: String)] = [0: (0, "")]
//
var convertTargetElements: [ConvertTargetElement] = []
for (idx, element) in self.input.enumerated() {
//
Self.updateConvertTargetElements(currentElements: &convertTargetElements, newElement: element)
//
let currentSurface = convertTargetElements.reduce(into: "") { $0 += $1.string }
// idx =
// idx + 1
map[idx + 1] = (currentSurface.count, currentSurface)
}
//
let finalSurface = convertTargetElements.reduce(into: "") { $0 += $1.string }
return map
.filter {
finalSurface.hasPrefix($0.value.surface)
}
.mapValues {
$0.surfaceIndex
}
}
public mutating func stopComposition() {
self.input = []
self.convertTarget = ""
self.convertTargetCursorPosition = 0
}
}
// MARK: API
// akafaakavalidkafinvalid
// ittaitvalid
extension ComposingText {
static func getConvertTarget(for elements: some Sequence<InputElement>) -> String {
var convertTargetElements: [ConvertTargetElement] = []
for element in elements {
updateConvertTargetElements(currentElements: &convertTargetElements, newElement: element)
}
return convertTargetElements.reduce(into: "") {$0 += $1.string}
}
static func shouldEscapeOtherValidation(convertTargetElement: [ConvertTargetElement], of originalElements: [InputElement]) -> Bool {
let string = convertTargetElement.reduce(into: "") {$0 += $1.string}
//
if !string.containsRomanAlphabet {
return true
}
if ["", "", "", ""].contains(string) {
return true
}
return false
}
static func isLeftSideValid(first firstElement: InputElement, of originalElements: [InputElement], from leftIndex: Int) -> Bool {
// leftIndex`el`
//
// * leftIndex == startIndex
// * el:direct
// * (_:direct) -> el
// * (a|i|u|e|o:roman2kana) -> el // akaka
// * (e-1:roman2kana and not n) && e-1 == es // ttatanna
// * (n:roman2kana) -> el && el not a|i|u|e|o|y|n // nkakanna
if leftIndex < originalElements.startIndex {
return false
}
// directElement
guard leftIndex != originalElements.startIndex && firstElement.inputStyle == .roman2kana else {
return true
}
let prevLastElement = originalElements[leftIndex - 1]
if prevLastElement.inputStyle != .roman2kana || !CharacterUtils.isRomanLetter(prevLastElement.character) {
return true
}
if ["a", "i", "u", "e", "o"].contains(prevLastElement.character) {
return true
}
if prevLastElement.character != "n" && prevLastElement.character == firstElement.character {
return true
}
let last_2 = originalElements[0 ..< leftIndex].suffix(2)
if ["zl", "zk", "zj", "zh", "xn"].contains(last_2.reduce(into: "") {$0.append($1.character)}) {
return true
}
let n_suffix = originalElements[0 ..< leftIndex].suffix(while: {$0.inputStyle == .roman2kana && $0.character == "n"})
// nnvalid
if n_suffix.count % 2 == 0 && !n_suffix.isEmpty {
return true
}
// nny-nnvalid
if n_suffix.count % 2 == 1 && !["a", "i", "u", "e", "o", "y", "n"].contains(firstElement.character) {
return true
}
// n1xvalid (xn)
if n_suffix.count % 2 == 1 && originalElements.dropLast(n_suffix.count).last == .init(character: "x", inputStyle: .roman2kana) {
return true
}
return false
}
/// valid調
/// - Parameters:
/// - lastElement:
/// - convertTargetElements: `convertTarget`
/// - originalElements: `input`
/// - rightIndex:
/// - Returns:
static func isRightSideValid(lastElement: InputElement, convertTargetElements: [ConvertTargetElement], of originalElements: [InputElement], to rightIndex: Int) -> Bool {
// rightIndexer
//
// * rightIndex == endIndex
// * er:direct
// * er -> (_:direct)
// * er == a|i|u|e|o // akaa
// * er != n && er -> er == e+1 // kkak
// * er == n && er -> (e+1:roman2kana and not a|i|u|e|o|n|y) // (nn)*nka(nn)*n
// * er == n && er -> (e+1:roman2kana) // (nn)*ann
// directElement
guard rightIndex != originalElements.endIndex && lastElement.inputStyle == .roman2kana else {
return true
}
if lastElement.inputStyle != .roman2kana {
return true
}
let nextFirstElement = originalElements[rightIndex]
if nextFirstElement.inputStyle != .roman2kana || !CharacterUtils.isRomanLetter(nextFirstElement.character) {
return true
}
if ["a", "i", "u", "e", "o"].contains(lastElement.character) {
return true
}
if lastElement.character != "n" && lastElement.character == nextFirstElement.character {
return true
}
guard let lastConvertTargetElements = convertTargetElements.last else {
return false
}
// nn
if lastElement.character == "n" && lastConvertTargetElements.string.last != "n" {
return true
}
// n1character
if lastElement.character == "n" && lastConvertTargetElements.inputStyle == .roman2kana && lastConvertTargetElements.string.last == "n" && !["a", "i", "u", "e", "o", "y", "n"].contains(nextFirstElement.character) {
return true
}
return false
}
///
/// - Parameters:
/// - lastElement:
/// - originalElements: `input`
/// - rightIndex:
/// - convertTargetElements: `convertTarget`
/// - Returns: valid`convertTarget`invalid`nil`
/// - Note: `elements = [r(k, a, n, s, h, a)]``k,a,n,s,h,a``k, a``a, n``s, h``k, a, n`
static func getConvertTargetIfRightSideIsValid(lastElement: InputElement, of originalElements: [InputElement], to rightIndex: Int, convertTargetElements: [ConvertTargetElement]) -> [Character]? {
debug(#function, lastElement, rightIndex)
if originalElements.endIndex < rightIndex {
return nil
}
//
// convertTarget
let shouldEscapeValidation = Self.shouldEscapeOtherValidation(convertTargetElement: convertTargetElements, of: originalElements)
if !shouldEscapeValidation && !Self.isRightSideValid(lastElement: lastElement, convertTargetElements: convertTargetElements, of: originalElements, to: rightIndex) {
return nil
}
// valid
var convertTargetElements = convertTargetElements
if let lastElement = convertTargetElements.last, lastElement.inputStyle == .roman2kana, rightIndex < originalElements.endIndex {
let nextFirstElement = originalElements[rightIndex]
if !lastElement.string.hasSuffix("n") && lastElement.string.last == nextFirstElement.character && CharacterUtils.isRomanLetter(nextFirstElement.character) {
//
convertTargetElements[convertTargetElements.endIndex - 1].string.removeLast()
convertTargetElements.append(ConvertTargetElement(string: [""], inputStyle: .direct))
}
if lastElement.string.hasSuffix("n") && !["a", "i", "u", "e", "o", "y", "n"].contains(nextFirstElement.character) {
//
convertTargetElements[convertTargetElements.endIndex - 1].string.removeLast()
convertTargetElements.append(ConvertTargetElement(string: [""], inputStyle: .direct))
}
}
return convertTargetElements.reduce(into: []) {$0 += $1.string}
}
// inputStyle
// k, o, r, e, h, ap, e, nd, e, s, u
// originalInput[ElementComposition(, roman2kana), ElementComposition(pen, direct), ElementComposition(, roman2kana)]
struct ConvertTargetElement {
var string: [Character]
var inputStyle: InputStyle
}
static func updateConvertTargetElements(currentElements: inout [ConvertTargetElement], newElement: InputElement) {
// currentElements
// ElementConvertTargetElement
if currentElements.last?.inputStyle != newElement.inputStyle {
currentElements.append(ConvertTargetElement(string: updateConvertTarget(current: [], inputStyle: newElement.inputStyle, newCharacter: newElement.character), inputStyle: newElement.inputStyle))
return
}
//
updateConvertTarget(&currentElements[currentElements.endIndex - 1].string, inputStyle: newElement.inputStyle, newCharacter: newElement.character)
}
static func updateConvertTarget(current: [Character], inputStyle: InputStyle, newCharacter: Character) -> [Character] {
switch inputStyle {
case .direct:
return current + [newCharacter]
case .roman2kana:
return Roman2Kana.toHiragana(currentText: current, added: newCharacter)
}
}
static func updateConvertTarget(_ convertTarget: inout [Character], inputStyle: InputStyle, newCharacter: Character) {
switch inputStyle {
case .direct:
convertTarget.append(newCharacter)
case .roman2kana:
convertTarget = Roman2Kana.toHiragana(currentText: convertTarget, added: newCharacter)
}
}
}
// Equatable
extension ComposingText: Equatable {}
extension ComposingText.InputElement: Equatable {}
extension ComposingText.ConvertTargetElement: Equatable {}
// MARK: API
extension ComposingText {
/// 2`ComposingText`
/// `convertTarget``convertTarget`
func differenceSuffix(to previousData: ComposingText) -> (deletedInput: Int, addedInput: Int, deletedSurface: Int, addedSurface: Int) {
// kshx ... last
// n ssss
// |
// inputdirect
//
let common = self.input.commonPrefix(with: previousData.input)
let deleted = previousData.input.count - common.count
let added = self.input.dropFirst(common.count).count
let commonSurface = self.convertTarget.commonPrefix(with: previousData.convertTarget)
let deletedSurface = previousData.convertTarget.count - commonSurface.count
let addedSurface = self.convertTarget.suffix(from: commonSurface.startIndex).count
return (deleted, added, deletedSurface, addedSurface)
}
func inputHasSuffix(inputOf suffix: ComposingText) -> Bool {
self.input.hasSuffix(suffix.input)
}
}
#if DEBUG
extension ComposingText.InputElement: CustomDebugStringConvertible {
public var debugDescription: String {
switch self.inputStyle {
case .direct:
return "direct(\(character))"
case .roman2kana:
return "roman2kana(\(character))"
}
}
}
extension ComposingText.ConvertTargetElement: CustomDebugStringConvertible {
var debugDescription: String {
"ConvertTargetElement(string: \"\(string)\", inputStyle: \(inputStyle)"
}
}
extension InputStyle: CustomDebugStringConvertible {
public var debugDescription: String {
"." + self.rawValue
}
}
#endif