Compare commits

...

51 Commits
3.8.1 ... main

Author SHA1 Message Date
ShikiSuen abb17e9ff6 [VersionUp] 3.8.5 GM Build 3850. 2024-04-07 00:56:09 +08:00
ShikiSuen afad2076b0 Update Data - 20240407 2024-04-07 00:26:41 +08:00
ShikiSuen 40245cf1d5 ToolTipUI // Enable round panel corners on old systems. 2024-04-06 17:07:13 +08:00
ShikiSuen 08a54c14c1 PCB // Enable round panel corners. 2024-04-06 16:38:26 +08:00
ShikiSuen 67a6bb5f87 LMMgr && Main // Allow dumping user dicts through terminal. 2024-04-02 19:12:35 +08:00
ShikiSuen 97a9e9aa5c LMAssembly // Let LMInstantiator summarize user data. 2024-04-02 19:12:35 +08:00
ShikiSuen c932083c5f LMInstantiator // Make async optional while loading user dicts. 2024-04-02 18:47:18 +08:00
ShikiSuen 25d8f7c093 LMInstantiator // Stop pinning default user weights for single reading. 2024-03-30 18:38:45 +08:00
ShikiSuen 14adf03311 LMInstantiator // Differentiate scores from factory results. 2024-03-30 18:38:45 +08:00
ShikiSuen 817df50916 LMMgr.UserPhrase // Fine-tweak suggestNextFreq(). 2024-03-30 18:31:06 +08:00
ShikiSuen e8961ff33f SessionCtl // Switch to .ofEmpty() state on toggling CapsLock. 2024-03-21 19:58:41 +08:00
ShikiSuen 2d23deb83a Tekkon // Update to v1.6.0 release. 2024-03-21 16:50:49 +08:00
ShikiSuen fc5243c97f AppDelegate // Update max RAM threshold to 1024MB. 2024-03-21 16:50:46 +08:00
ShikiSuen 9e1d130ba7 UserDef // +kMinCellWidthForHorizontalMatrix. 2024-03-10 22:00:51 +08:00
ShikiSuen 5a6aee2a25 PCB // Tweak panel opacity. 2024-03-10 22:00:51 +08:00
ShikiSuen aa4162fa9b DataCompiler // Fix SQLite random segmentation fault 11. 2024-03-10 00:03:54 +08:00
ShikiSuen 951f41461a TDKCandidates // Refactor highlightedColor(). 2024-03-09 04:14:59 +08:00
ShikiSuen f46cfda6f5 [VersionUp] 3.8.4 GM Build 3840. 2024-03-08 03:21:39 +08:00
ShikiSuen 03edccff4f Update Data - 20240308 2024-03-08 03:18:05 +08:00
ShikiSuen d8aba434d9 SecureEventInputSputnik // Patch a memory leak, etc. 2024-03-08 01:54:11 +08:00
ShikiSuen 005116c429 SPM // Consolidate dependencies. 2024-03-06 00:18:55 +08:00
ShikiSuen 93256f0095 Xcode // Add a debuggable-only target. 2024-03-04 18:34:28 +08:00
ShikiSuen 11bb5a9b66 Xcode // Stop stripping Swift symbols. 2024-03-04 17:32:25 +08:00
ShikiSuen 9dc7821708 [VersionUp] 3.8.3 GM Build 3830. 2024-03-02 23:06:53 +08:00
ShikiSuen 5d62d5b66d Update Data - 20240301 2024-03-02 23:06:53 +08:00
ShikiSuen c63c531f1b MainAssembly // Include remaining AppDelegate IBOutlets. 2024-03-02 23:06:53 +08:00
ShikiSuen 35d4426730 UserPhrase // Improve score boosting / nerfing for single kanji. 2024-03-02 23:06:53 +08:00
ShikiSuen b628ddd082 LMInstantiator // Expose factoryCoreUnigramsFor(). 2024-03-02 23:06:53 +08:00
ShikiSuen 4e00791144 LMPlainBopomofo // Fix mistakes in Eten DOS CHS Sequence Data. 2024-03-02 23:06:53 +08:00
ShikiSuen 1e098cac53 InputHandler // Prioritize the handling of the service menu. 2024-03-02 23:06:53 +08:00
ShikiSuen 0107e7cd78 ServiceMenu // Filter some services if readings are unavailable. 2024-03-02 23:06:53 +08:00
ShikiSuen 9411686d03 UserDef // +useShiftQuestionToCallServiceMenu. 2024-03-02 23:06:53 +08:00
ShikiSuen 5eec7cd604 MainAssembly // + Candidate Service (Menu & Editor). 2024-03-02 23:06:53 +08:00
ShikiSuen dc79c629a1 CandidateNode // Subclass: ServiceMenuNode. 2024-03-02 23:06:53 +08:00
ShikiSuen 040c597345 Shared // +CandidateTextService. 2024-03-02 23:06:53 +08:00
ShikiSuen 923471c8bb UserDef // +kCandidateServiceMenuContents. 2024-03-02 23:06:53 +08:00
ShikiSuen 46d4e7bdb3 MainAssembly // Refactor wherever using UniformTypeIdentifiers. 2024-03-02 23:06:53 +08:00
ShikiSuen 55dcdc8ce0 (NS)String // Add some codepoint extensions. 2024-03-02 23:06:53 +08:00
ShikiSuen bd5fdcaa26 ClientListMgr // Fix metrics and the invisible scroller. 2024-03-02 23:06:53 +08:00
ShikiSuen d5d9167b1e CocoaImpl // Fix isDarkMode(). 2024-03-02 23:06:53 +08:00
ShikiSuen c2679735c1 PrefMgr // Refactor the didSet methods. 2024-03-02 23:06:53 +08:00
ShikiSuen 4904664277 Shared // Fix a KVO Observer. 2024-03-02 23:06:53 +08:00
ShikiSuen 76dd75ce5a UserDef // +kSpecifyCmdOptCtrlEnterBehavior. 2024-03-02 23:06:53 +08:00
ShikiSuen 7be2a85b25 BrailleSputnik // Initial Implementation. 2024-03-02 23:06:53 +08:00
ShikiSuen 549c361af4 [VersionUp] 3.8.2 GM Build 3820. 2024-03-02 23:06:53 +08:00
ShikiSuen 72655119fa Update Data - 20240223 2024-02-24 03:56:21 +08:00
ShikiSuen 3ebb5f2f48 LMAssembly // Integrate EtenDOS SCPC data into the codebase. 2024-02-23 14:01:30 +08:00
ShikiSuen e44843e603 LMAssembly // Pack LMUserOverride inside LMInstantiator, etc. 2024-02-23 13:55:29 +08:00
ShikiSuen c5899152e6 InputHandler // Move some case-switch results to InputMode enum. 2024-02-21 14:58:26 +08:00
ShikiSuen 275288ea61 Hotenka // Deprecate NSJSONSerialization. 2024-02-19 01:33:47 +08:00
ShikiSuen 4184c3c1d2 DataCompiler // Post-dump SQLite database. 2024-02-18 23:58:06 +08:00
173 changed files with 12748 additions and 8878 deletions

View File

@ -194,7 +194,7 @@ func prepareDatabase() -> Bool {
PRIMARY KEY (theChar) PRIMARY KEY (theChar)
) WITHOUT ROWID; ) WITHOUT ROWID;
""" """
guard sqlite3_open(urlSQLite, &ptrSQL) == SQLITE_OK else { return false } guard sqlite3_open(":memory:", &ptrSQL) == SQLITE_OK else { return false }
guard sqlite3_exec(ptrSQL, "PRAGMA synchronous = OFF;", nil, nil, nil) == SQLITE_OK else { return false } guard sqlite3_exec(ptrSQL, "PRAGMA synchronous = OFF;", nil, nil, nil) == SQLITE_OK else { return false }
guard sqlite3_exec(ptrSQL, "PRAGMA journal_mode = OFF;", nil, nil, nil) == SQLITE_OK else { return false } guard sqlite3_exec(ptrSQL, "PRAGMA journal_mode = OFF;", nil, nil, nil) == SQLITE_OK else { return false }
guard sqlMakeTableMACV.runAsSQLExec(dbPointer: &ptrSQL) else { return false } guard sqlMakeTableMACV.runAsSQLExec(dbPointer: &ptrSQL) else { return false }
@ -231,6 +231,20 @@ func prepareDatabase() -> Bool {
return true return true
} }
// MARK: - Dump SQLite3 Memory Database to File.
@discardableResult func dumpSQLDB() -> Bool {
var ptrSQLTarget: OpaquePointer?
defer { sqlite3_close_v2(ptrSQLTarget) }
guard sqlite3_open(urlSQLite, &ptrSQLTarget) == SQLITE_OK else { return false }
let ptrBackupObj = sqlite3_backup_init(ptrSQLTarget, "main", ptrSQL, "main")
if ptrBackupObj != nil {
sqlite3_backup_step(ptrBackupObj, -1)
sqlite3_backup_finish(ptrBackupObj)
}
return sqlite3_errcode(ptrSQLTarget) == SQLITE_OK
}
// MARK: - // MARK: -
func rawDictForPhrases(isCHS: Bool) -> [Unigram] { func rawDictForPhrases(isCHS: Bool) -> [Unigram] {
@ -1044,6 +1058,19 @@ func healthCheck(_ data: [Unigram]) -> String {
return result return result
} }
// MARK: - Flags
struct TaskFlags: OptionSet {
public let rawValue: Int
public init(rawValue: Int) {
self.rawValue = rawValue
}
public static let common = TaskFlags(rawValue: 1 << 0)
public static let chs = TaskFlags(rawValue: 1 << 1)
public static let cht = TaskFlags(rawValue: 1 << 2)
}
// MARK: - // MARK: -
var compileJSON = false var compileJSON = false
@ -1066,57 +1093,71 @@ func main() {
NSLog("// SQLite 資料庫初期化失敗。") NSLog("// SQLite 資料庫初期化失敗。")
exit(-1) exit(-1)
} }
var taskFlags: TaskFlags = [.common, .chs, .cht] {
didSet {
guard taskFlags.isEmpty else { return }
NSLog("// 全部 TXT 辭典檔案建置完畢。")
if compileJSON {
NSLog("// 全部 JSON 辭典檔案建置完畢。")
}
if compileSQLite, prepared {
NSLog("// 開始整合反查資料。")
mapReverseLookupForCheck.forEach { key, values in
values.reversed().forEach { valueLiteral in
let value = cnvPhonabetToASCII(valueLiteral)
if !rangeMapReverseLookup[key, default: []].contains(value) {
rangeMapReverseLookup[key, default: []].insert(value, at: 0)
}
}
}
NSLog("// 反查資料整合完畢。")
NSLog("// 準備建置 SQL 資料庫。")
writeMainMapToSQL(rangeMapJSONCHS, column: "theDataCHS")
writeMainMapToSQL(rangeMapJSONCHT, column: "theDataCHT")
writeMainMapToSQL(rangeMapSymbols, column: "theDataSYMB")
writeMainMapToSQL(rangeMapZhuyinwen, column: "theDataCHEW")
writeMainMapToSQL(rangeMapCNS, column: "theDataCNS")
writeRevLookupMapToSQL(rangeMapReverseLookup)
let committed = "commit;".runAsSQLExec(dbPointer: &ptrSQL)
assert(committed)
let compressed = "VACUUM;".runAsSQLExec(dbPointer: &ptrSQL)
assert(compressed)
if !dumpSQLDB() {
NSLog("// SQLite 辭典傾印失敗。")
} else {
NSLog("// 全部 SQLite 辭典檔案建置完畢。")
}
sqlite3_close_v2(ptrSQL)
}
}
}
let globalQueue = DispatchQueue.global(qos: .default) let globalQueue = DispatchQueue.global(qos: .default)
let group = DispatchGroup() let group = DispatchGroup()
group.enter() group.enter()
globalQueue.async { globalQueue.async {
NSLog("// 準備編譯符號表情ㄅ文語料檔案。") NSLog("// 準備編譯符號表情ㄅ文語料檔案。")
commonFileOutput() commonFileOutput()
taskFlags.remove(.common)
group.leave() group.leave()
} }
group.enter() group.enter()
globalQueue.async { globalQueue.async {
NSLog("// 準備編譯繁體中文核心語料檔案。") NSLog("// 準備編譯繁體中文核心語料檔案。")
fileOutput(isCHS: false) fileOutput(isCHS: false)
taskFlags.remove(.cht)
group.leave() group.leave()
} }
group.enter() group.enter()
globalQueue.async { globalQueue.async {
NSLog("// 準備編譯簡體中文核心語料檔案。") NSLog("// 準備編譯簡體中文核心語料檔案。")
fileOutput(isCHS: true) fileOutput(isCHS: true)
taskFlags.remove(.chs)
group.leave() group.leave()
} }
// //
group.wait() group.wait()
NSLog("// 全部 TXT 辭典檔案建置完畢。")
if compileJSON {
NSLog("// 全部 JSON 辭典檔案建置完畢。")
}
if compileSQLite, prepared {
NSLog("// 開始整合反查資料。")
mapReverseLookupForCheck.forEach { key, values in
values.reversed().forEach { valueLiteral in
let value = cnvPhonabetToASCII(valueLiteral)
if !rangeMapReverseLookup[key, default: []].contains(value) {
rangeMapReverseLookup[key, default: []].insert(value, at: 0)
}
}
}
NSLog("// 反查資料整合完畢。")
NSLog("// 準備建置 SQL 資料庫。")
writeMainMapToSQL(rangeMapJSONCHS, column: "theDataCHS")
writeMainMapToSQL(rangeMapJSONCHT, column: "theDataCHT")
writeMainMapToSQL(rangeMapSymbols, column: "theDataSYMB")
writeMainMapToSQL(rangeMapZhuyinwen, column: "theDataCHEW")
writeMainMapToSQL(rangeMapCNS, column: "theDataCNS")
writeRevLookupMapToSQL(rangeMapReverseLookup)
let committed = "commit;".runAsSQLExec(dbPointer: &ptrSQL)
assert(committed)
let compressed = "VACUUM;".runAsSQLExec(dbPointer: &ptrSQL)
assert(compressed)
sqlite3_close_v2(ptrSQL)
NSLog("// 全部 SQLite 辭典檔案建置完畢。")
}
} }
main() main()

View File

@ -13,13 +13,13 @@ let package = Package(
), ),
], ],
dependencies: [ dependencies: [
.package(path: "../vChewing_CocoaExtension"), .package(path: "../vChewing_OSFrameworkImpl"),
], ],
targets: [ targets: [
.target( .target(
name: "NSAttributedTextView", name: "NSAttributedTextView",
dependencies: [ dependencies: [
.product(name: "CocoaExtension", package: "vChewing_CocoaExtension"), .product(name: "OSFrameworkImpl", package: "vChewing_OSFrameworkImpl"),
] ]
), ),
.testTarget( .testTarget(

View File

@ -6,7 +6,7 @@
// Modified by The vChewing Project in order to use it with AppKit. // Modified by The vChewing Project in order to use it with AppKit.
import AppKit import AppKit
import CocoaExtension import OSFrameworkImpl
import SwiftUI import SwiftUI
@available(macOS 10.15, *) @available(macOS 10.15, *)

View File

@ -7,9 +7,9 @@
// requirements defined in MIT License. // requirements defined in MIT License.
import AppKit import AppKit
import CocoaExtension
import Foundation import Foundation
@testable import NSAttributedTextView @testable import NSAttributedTextView
import OSFrameworkImpl
import Shared import Shared
import XCTest import XCTest

View File

@ -1,7 +1,6 @@
// (c) 2019 and onwards Robert Muckle-Jones (Apache 2.0 License). // (c) 2019 and onwards Robert Muckle-Jones (Apache 2.0 License).
import Foundation import Foundation
import SwiftExtension
public class LineReader { public class LineReader {
let encoding: String.Encoding let encoding: String.Encoding

View File

@ -1,9 +1,8 @@
.DS_Store .DS_Store
/.build /.build
/Packages /Packages
/*.xcodeproj
xcuserdata/ xcuserdata/
DerivedData/ DerivedData/
.swiftpm/config/registries.json .swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc .netrc

View File

@ -0,0 +1,35 @@
// swift-tools-version: 5.7
import PackageDescription
let package = Package(
name: "BrailleSputnik",
platforms: [
.macOS(.v11),
],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "BrailleSputnik",
targets: ["BrailleSputnik"]
),
],
dependencies: [
.package(path: "../vChewing_Shared"),
.package(path: "../vChewing_Tekkon"),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "BrailleSputnik",
dependencies: [
.product(name: "Shared", package: "vChewing_Shared"),
.product(name: "Tekkon", package: "vChewing_Tekkon"),
]
),
.testTarget(
name: "BrailleSputnikTests",
dependencies: ["BrailleSputnik"]
),
]
)

View File

@ -0,0 +1,284 @@
// (c) 2022 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
extension BrailleSputnik {
enum Braille: String {
case blank = "" // U+2800
case d1 = ""
case d2 = ""
case d12 = ""
case d3 = ""
case d13 = ""
case d23 = ""
case d123 = ""
case d4 = ""
case d14 = ""
case d24 = ""
case d124 = ""
case d34 = ""
case d134 = ""
case d234 = ""
case d1234 = ""
case d5 = ""
case d15 = ""
case d25 = ""
case d125 = ""
case d35 = ""
case d135 = ""
case d235 = ""
case d1235 = ""
case d45 = ""
case d145 = ""
case d245 = ""
case d1245 = ""
case d345 = ""
case d1345 = ""
case d2345 = ""
case d12345 = ""
case d6 = ""
case d16 = ""
case d26 = ""
case d126 = ""
case d36 = ""
case d136 = ""
case d236 = ""
case d1236 = ""
case d46 = ""
case d146 = ""
case d246 = ""
case d1246 = ""
case d346 = ""
case d1346 = ""
case d2346 = ""
case d12346 = ""
case d56 = ""
case d156 = ""
case d256 = ""
case d1256 = ""
case d356 = ""
case d1356 = ""
case d2356 = ""
case d12356 = ""
case d456 = ""
case d1456 = ""
case d2456 = ""
case d12456 = ""
case d3456 = ""
case d13456 = ""
case d23456 = ""
case d123456 = ""
}
public enum BrailleStandard: Int {
case of1947 = 1
case of2018 = 2
}
}
protocol BrailleProcessingUnit {
var mapConsonants: [String: String] { get }
var mapSemivowels: [String: String] { get }
var mapVowels: [String: String] { get }
var mapIntonations: [String: String] { get }
var mapIntonationSpecialCases: [String: String] { get }
var mapCombinedVowels: [String: String] { get }
var mapPunctuations: [String: String] { get }
func handleSpecialCases(target: inout String, value: String?) -> Bool
}
// MARK: - Static Data conforming to 1947 Standard.
extension BrailleSputnik {
class BrailleProcessingUnit1947: BrailleProcessingUnit {
func handleSpecialCases(target _: inout String, value _: String?) -> Bool {
//
false
}
let mapConsonants: [String: String] = [
"": "", "": "", "": "",
"": "", "": "", "": "",
"": "", "": "", "": "",
"": "", "": "", "": "",
"": "", "": "", "": "",
"": "", "": "", "": "",
"": "", "": "", "": "",
]
let mapSemivowels: [String: String] = [
"": "", "": "", "": "",
]
let mapVowels: [String: String] = [
"": "", "": "", "": "",
"": "", "": "", "": "",
"": "", "": "", "": "",
"": "", "": "", "": "",
]
let mapIntonations: [String: String] = [
"˙": "⠱⠁", "ˇ": "", "ˊ": "", " ": "", "ˋ": "",
]
let mapIntonationSpecialCases: [String: String] = [
"ㄜ˙": "⠮⠁", "ㄚ˙": "⠜⠁", "ㄛ˙": "⠣⠁", "ㄣ˙": "⠥⠁",
]
let mapCombinedVowels: [String: String] = [
"ㄧㄝ": "", "ㄧㄣ": "", "ㄩㄝ": "",
"ㄨㄟ": "", "ㄨㄥ": "", "ㄨㄣ": "",
"ㄨㄚ": "", "ㄧㄡ": "", "ㄧㄤ": "",
"ㄧㄚ": "", "ㄨㄛ": "", "ㄧㄥ": "",
"ㄨㄞ": "", "ㄩㄥ": "", "ㄧㄠ": "",
"ㄧㄞ": "", "ㄨㄤ": "", "ㄩㄣ": "",
"ㄧㄢ": "", "ㄩㄢ": "", "ㄨㄢ": "",
]
let mapPunctuations: [String: String] = [
"": "⠤⠀", "·": "⠤⠀", "": "", "": "",
"": "", "": "⠕⠀", "": "⠇⠀", "": "⠒⠒",
"╴╴": "⠰⠰", "": "⠠⠤", "……": "⠐⠐⠐",
"": "⠐⠂", "—— ——": "⠐⠂⠐⠂", "": "⠈⠼", "": "⠪⠕",
"": "⠦⠦", "": "⠴⠴", "": "⠰⠤", "": "⠤⠆",
"": "⠦⠦", "": "⠴⠴", "": "⠰⠤", "": "⠤⠆",
"": "", "": "", "": "", "": "",
"": "", "": "", "": "", "": "",
]
}
}
// MARK: - Static Data conforming to 2018 Standard (GF0019-2018)
extension BrailleSputnik {
class BrailleProcessingUnit2018: BrailleProcessingUnit {
func handleSpecialCases(target: inout String, value: String?) -> Bool {
guard let value = value else { return false }
switch value {
case "": target = Braille.d2345.rawValue + Braille.d35.rawValue
case "": target = Braille.d4.rawValue + Braille.d2345.rawValue + Braille.d35.rawValue
default: return false
}
return true
}
let mapConsonants: [String: String] = [
"": Braille.d12.rawValue,
"": Braille.d1234.rawValue,
"": Braille.d134.rawValue,
"": Braille.d124.rawValue,
"": Braille.d145.rawValue,
"": Braille.d2345.rawValue,
"": Braille.d1345.rawValue,
"": Braille.d123.rawValue,
"": Braille.d1245.rawValue,
"": Braille.d13.rawValue,
"": Braille.d125.rawValue,
"": Braille.d1245.rawValue,
"": Braille.d13.rawValue,
"": Braille.d125.rawValue,
"": Braille.d34.rawValue,
"": Braille.d12345.rawValue,
"": Braille.d156.rawValue,
"": Braille.d245.rawValue,
"": Braille.d1356.rawValue,
"": Braille.d14.rawValue,
"": Braille.d234.rawValue,
]
let mapSemivowels: [String: String] = [
"": Braille.d24.rawValue,
"": Braille.d136.rawValue,
"": Braille.d346.rawValue,
]
let mapVowels: [String: String] = [
"": Braille.d35.rawValue,
"": Braille.d26.rawValue,
"": Braille.d26.rawValue,
"": Braille.d246.rawValue,
"": Braille.d2346.rawValue,
"": Braille.d235.rawValue,
"": Braille.d12356.rawValue,
"": Braille.d1236.rawValue,
"": Braille.d356.rawValue,
"": Braille.d236.rawValue,
"": Braille.d3456.rawValue, //
"": Braille.d1235.rawValue,
]
let mapIntonations: [String: String] = [
" ": Braille.d1.rawValue,
"ˊ": Braille.d2.rawValue,
"ˇ": Braille.d3.rawValue,
"ˋ": Braille.d23.rawValue,
// "˙": nil, //
]
let mapIntonationSpecialCases: [String: String] = [:]
let mapCombinedVowels: [String: String] = [
"ㄧㄚ": Braille.d1246.rawValue,
"ㄧㄝ": Braille.d15.rawValue,
"ㄧㄞ": Braille.d1246.rawValue, //
"ㄧㄠ": Braille.d345.rawValue,
"ㄧㄡ": Braille.d1256.rawValue,
"ㄧㄢ": Braille.d146.rawValue,
"ㄧㄣ": Braille.d126.rawValue,
"ㄧㄤ": Braille.d1346.rawValue,
"ㄧㄥ": Braille.d16.rawValue,
"ㄨㄚ": Braille.d123456.rawValue,
"ㄨㄛ": Braille.d135.rawValue,
"ㄨㄞ": Braille.d13456.rawValue,
"ㄨㄟ": Braille.d2456.rawValue,
"ㄨㄢ": Braille.d12456.rawValue,
"ㄨㄣ": Braille.d25.rawValue,
"ㄨㄤ": Braille.d2356.rawValue,
"ㄨㄥ": Braille.d256.rawValue,
"ㄩㄝ": Braille.d23456.rawValue,
"ㄩㄢ": Braille.d12346.rawValue,
"ㄩㄣ": Braille.d456.rawValue,
"ㄩㄥ": Braille.d1456.rawValue,
]
let mapPunctuations: [String: String] = [
"": Braille.d5.rawValue + Braille.d23.rawValue,
"·": Braille.d6.rawValue + Braille.d3.rawValue,
"": Braille.d5.rawValue,
"": Braille.d56.rawValue,
"": Braille.d4.rawValue,
"": Braille.d5.rawValue + Braille.d3.rawValue,
"": Braille.d56.rawValue + Braille.d2.rawValue,
"": Braille.d36.rawValue,
"——": Braille.d6.rawValue + Braille.d36.rawValue,
"……": Braille.d5.rawValue + Braille.d5.rawValue + Braille.d5.rawValue,
"-": Braille.d36.rawValue,
"": Braille.d5.rawValue, //
"": Braille.d2356.rawValue + Braille.d35.rawValue,
"": Braille.d5.rawValue + Braille.d36.rawValue,
"": Braille.d36.rawValue + Braille.d2.rawValue,
"": Braille.d5.rawValue + Braille.d3.rawValue,
"": Braille.d6.rawValue + Braille.d2.rawValue,
"": Braille.d45.rawValue + Braille.d45.rawValue,
"": Braille.d45.rawValue + Braille.d45.rawValue,
"": Braille.d45.rawValue,
"": Braille.d45.rawValue,
"": Braille.d45.rawValue + Braille.d45.rawValue,
"": Braille.d45.rawValue + Braille.d45.rawValue,
"": Braille.d45.rawValue,
"": Braille.d45.rawValue,
"": Braille.d56.rawValue + Braille.d3.rawValue,
"": Braille.d6.rawValue + Braille.d23.rawValue,
"": Braille.d56.rawValue + Braille.d23.rawValue,
"": Braille.d56.rawValue + Braille.d23.rawValue,
"": Braille.d56.rawValue + Braille.d23.rawValue,
"": Braille.d56.rawValue + Braille.d23.rawValue,
// "": "", "": "", // 2018
]
}
}

View File

@ -0,0 +1,109 @@
// (c) 2022 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import Tekkon
public class BrailleSputnik {
public var standard: BrailleStandard
public init(standard: BrailleStandard) {
self.standard = standard
}
var staticData: BrailleProcessingUnit {
switch standard {
case .of1947: return Self.staticData1947
case .of2018: return Self.staticData2018
}
}
static var sharedComposer = Tekkon.Composer("", arrange: .ofDachen, correction: true)
private static let staticData1947: BrailleProcessingUnit = BrailleProcessingUnit1947()
private static let staticData2018: BrailleProcessingUnit = BrailleProcessingUnit2018()
}
public extension BrailleSputnik {
func convertToBraille(smashedPairs: [(key: String, value: String)], extraInsertion: (reading: String, cursor: Int)? = nil) -> String {
var convertedStack: [String?] = []
var processedKeysCount = 0
var extraInsertion = extraInsertion
smashedPairs.forEach { key, value in
let subKeys = key.split(separator: "\t")
switch subKeys.count {
case 0: return
case 1:
guard !key.isEmpty else { break }
let isPunctuation: Bool = key.first == "_" //
if isPunctuation {
convertedStack.append(convertPunctuationToBraille(value))
} else {
var key = key.description
fixToneOne(target: &key)
convertedStack.append(convertPhonabetReadingToBraille(key, value: value))
}
processedKeysCount += 1
default:
//
subKeys.forEach { subKey in
var subKey = subKey.description
fixToneOne(target: &subKey)
convertedStack.append(convertPhonabetReadingToBraille(subKey))
processedKeysCount += 1
}
}
if let theExtraInsertion = extraInsertion, processedKeysCount == theExtraInsertion.cursor {
convertedStack.append(convertPhonabetReadingToBraille(theExtraInsertion.reading))
extraInsertion = nil
}
}
return convertedStack.compactMap(\.?.description).joined()
}
private func fixToneOne(target key: inout String) {
for char in key {
guard Tekkon.Phonabet(char.description).type != .null else { return }
}
if let lastChar = key.last?.description, Tekkon.Phonabet(lastChar).type != .intonation {
key += " "
}
}
func convertPunctuationToBraille(_ givenTarget: any StringProtocol) -> String? {
staticData.mapPunctuations[givenTarget.description]
}
func convertPhonabetReadingToBraille(_ rawReading: any StringProtocol, value referredValue: String? = nil) -> String? {
var resultStack = ""
//
guard !staticData.handleSpecialCases(target: &resultStack, value: referredValue) else { return resultStack }
Self.sharedComposer.clear()
rawReading.forEach { char in
Self.sharedComposer.receiveKey(fromPhonabet: char.description)
}
let consonant = Self.sharedComposer.consonant.value
let semivowel = Self.sharedComposer.semivowel.value
let vowel = Self.sharedComposer.vowel.value
let intonation = Self.sharedComposer.intonation.value
if !consonant.isEmpty {
resultStack.append(staticData.mapConsonants[consonant] ?? "")
}
let combinedVowels = Self.sharedComposer.semivowel.value + Self.sharedComposer.vowel.value
if combinedVowels.count == 2 {
resultStack.append(staticData.mapCombinedVowels[combinedVowels] ?? "")
} else {
resultStack.append(staticData.mapSemivowels[semivowel] ?? "")
resultStack.append(staticData.mapVowels[vowel] ?? "")
}
// 調
if let intonationSpecialCaseMetResult = staticData.mapIntonationSpecialCases[vowel + intonation] {
resultStack.append(intonationSpecialCaseMetResult.last?.description ?? "")
} else {
resultStack.append(staticData.mapIntonations[intonation] ?? "")
}
return resultStack
}
}

View File

@ -0,0 +1,28 @@
// (c) 2022 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
@testable import BrailleSputnik
import XCTest
final class BrailleSputnikTests: XCTestCase {
func testBrailleConversion() throws {
//
var rawReadingStr = "ㄉㄚˋ-ㄑㄧㄡ-ㄑㄧㄡ-ㄅㄧㄥˋ-ㄌㄜ˙-ㄦˋ-ㄑㄧㄡ-ㄑㄧㄡ-ㄑㄧㄠˊ-_"
rawReadingStr += "-ㄙㄢ-ㄑㄧㄡ-ㄑㄧㄡ-ㄘㄞˇ-ㄧㄠˋ-ㄙˋ-ㄑㄧㄡ-ㄑㄧㄡ-ㄠˊ-_。"
let rawReadingArray: [(key: String, value: String)] = rawReadingStr.split(separator: "-").map {
let value: String = $0.first == "_" ? $0.last?.description ?? "" : ""
return (key: $0.description, value: value)
}
let processor = BrailleSputnik(standard: .of1947)
let result1947 = processor.convertToBraille(smashedPairs: rawReadingArray)
XCTAssertEqual(result1947, "⠙⠜⠐⠚⠎⠄⠚⠎⠄⠕⠽⠐⠉⠮⠁⠱⠐⠚⠎⠄⠚⠎⠄⠚⠪⠂⠆⠑⠧⠄⠚⠎⠄⠚⠎⠄⠚⠺⠈⠪⠐⠑⠐⠚⠎⠄⠚⠎⠄⠩⠂⠤⠀")
processor.standard = .of2018
let result2018 = processor.convertToBraille(smashedPairs: rawReadingArray)
XCTAssertEqual(result2018, "⠙⠔⠆⠅⠳⠁⠅⠳⠁⠃⠡⠆⠇⠢⠗⠆⠅⠳⠁⠅⠳⠁⠅⠜⠂⠐⠎⠧⠁⠅⠳⠁⠅⠳⠁⠉⠪⠄⠜⠆⠎⠆⠅⠳⠁⠅⠳⠁⠖⠂⠐⠆")
}
}

View File

@ -19,6 +19,7 @@ public class CandidateCellData: Hashable {
public static var unifiedSize: Double = 16 public static var unifiedSize: Double = 16
public static var unifiedCharDimension: Double { ceil(unifiedSize * 1.0125 + 7) } public static var unifiedCharDimension: Double { ceil(unifiedSize * 1.0125 + 7) }
public static var unifiedTextHeight: Double { ceil(unifiedSize * 19 / 16) } public static var unifiedTextHeight: Double { ceil(unifiedSize * 19 / 16) }
static var internalPrefs = PrefMgr()
public var selectionKey: String public var selectionKey: String
public let displayedText: String public let displayedText: String
public private(set) var textDimension: NSSize public private(set) var textDimension: NSSize
@ -81,7 +82,8 @@ public class CandidateCellData: Hashable {
} }
public func cellLength(isMatrix: Bool = true) -> Double { public func cellLength(isMatrix: Bool = true) -> Double {
let minLength = ceil(Self.unifiedCharDimension * 2 + size * 1.25) let factor: CGFloat = (Self.internalPrefs.minCellWidthForHorizontalMatrix == 0) ? 1.5 : 2
let minLength = ceil(Self.unifiedCharDimension * factor + size * 1.25)
if displayedText.count <= 2, isMatrix { return minLength } if displayedText.count <= 2, isMatrix { return minLength }
return textDimension.width return textDimension.width
} }

View File

@ -17,26 +17,24 @@ open class CtlCandidate: NSWindowController, CtlCandidateProtocol {
open var reverseLookupResult: [String] = [] open var reverseLookupResult: [String] = []
open func highlightedColor() -> NSColor { open func highlightedColor() -> NSColor {
var result = NSColor.controlAccentColor var result = NSColor.clear
var colorBlendAmount: Double = NSApplication.isDarkMode ? 0.3 : 0.0 if #available(macOS 10.14, *) {
if #available(macOS 10.14, *), !NSApplication.isDarkMode, locale == "zh-Hant" { result = .controlAccentColor
colorBlendAmount = 0.15 } else {
result = .alternateSelectedControlTextColor
} }
let colorBlendAmount = 0.3
// //
switch locale { switch locale {
case "zh-Hans": case "zh-Hans":
result = NSColor.systemRed result = NSColor.red
case "zh-Hant": case "zh-Hant":
result = NSColor.systemBlue result = NSColor.blue
case "ja": case "ja":
result = NSColor.systemBrown result = NSColor.brown
default: break default: break
} }
var blendingAgainstTarget: NSColor = NSApplication.isDarkMode ? NSColor.black : NSColor.white let blendingAgainstTarget: NSColor = NSApplication.isDarkMode ? NSColor.black : NSColor.white
if #unavailable(macOS 10.14) {
colorBlendAmount = 0.3
blendingAgainstTarget = NSColor.white
}
return result.blended(withFraction: colorBlendAmount, of: blendingAgainstTarget)! return result.blended(withFraction: colorBlendAmount, of: blendingAgainstTarget)!
} }

View File

@ -7,7 +7,7 @@
// requirements defined in MIT License. // requirements defined in MIT License.
import AppKit import AppKit
import CocoaExtension import OSFrameworkImpl
import Shared import Shared
private extension NSUserInterfaceLayoutOrientation { private extension NSUserInterfaceLayoutOrientation {

View File

@ -7,7 +7,7 @@
// requirements defined in MIT License. // requirements defined in MIT License.
import AppKit import AppKit
import CocoaExtension import OSFrameworkImpl
import Shared import Shared
/// AppKit SwiftUI /// AppKit SwiftUI

View File

@ -100,9 +100,7 @@ public class HotenkaChineseConverter {
dictFiles = .init() dictFiles = .init()
do { do {
let rawData = try Data(contentsOf: URL(fileURLWithPath: jsonDir)) let rawData = try Data(contentsOf: URL(fileURLWithPath: jsonDir))
guard let rawJSON: [String: [String: String]] = try JSONSerialization.jsonObject(with: rawData) as? [String: [String: String]] else { let rawJSON = try JSONDecoder().decode([String: [String: String]].self, from: rawData)
throw NSError()
}
dict = rawJSON dict = rawJSON
} catch { } catch {
NSLog("// Exception happened when reading dict json at: \(jsonDir).") NSLog("// Exception happened when reading dict json at: \(jsonDir).")

View File

@ -40,7 +40,10 @@ extension HotenkaTests {
let testInstance: HotenkaChineseConverter = .init(dictDir: testDataPath) let testInstance: HotenkaChineseConverter = .init(dictDir: testDataPath)
NSLog("// Loading complete. Generating json dict file.") NSLog("// Loading complete. Generating json dict file.")
do { do {
try JSONSerialization.data(withJSONObject: testInstance.dict, options: .sortedKeys).write(to: URL(fileURLWithPath: testDataPath + "convdict.json")) let urlOutput = URL(fileURLWithPath: testDataPath + "convdict.json")
let encoder = JSONEncoder()
encoder.outputFormatting = .sortedKeys
try encoder.encode(testInstance.dict).write(to: urlOutput, options: .atomic)
} catch { } catch {
NSLog("// Error on writing strings to file: \(error)") NSLog("// Error on writing strings to file: \(error)")
} }

View File

@ -15,8 +15,7 @@ let package = Package(
dependencies: [ dependencies: [
.package(path: "../RMJay_LineReader"), .package(path: "../RMJay_LineReader"),
.package(path: "../vChewing_Megrez"), .package(path: "../vChewing_Megrez"),
.package(path: "../vChewing_PinyinPhonaConverter"), .package(path: "../vChewing_SwiftExtension"),
.package(path: "../vChewing_Shared"),
], ],
targets: [ targets: [
.target( .target(
@ -24,12 +23,7 @@ let package = Package(
dependencies: [ dependencies: [
.product(name: "LineReader", package: "RMJay_LineReader"), .product(name: "LineReader", package: "RMJay_LineReader"),
.product(name: "Megrez", package: "vChewing_Megrez"), .product(name: "Megrez", package: "vChewing_Megrez"),
.product(name: "Shared", package: "vChewing_Shared"), .product(name: "SwiftExtension", package: "vChewing_SwiftExtension"),
.product(name: "PinyinPhonaConverter", package: "vChewing_PinyinPhonaConverter"),
],
resources: [
.process("Resources/sequenceDataFromEtenDOS-chs.json"),
.process("Resources/sequenceDataFromEtenDOS-cht.json"),
] ]
), ),
.testTarget( .testTarget(

View File

@ -1,17 +1,17 @@
# LangModelAssembly # LangModelAssembly
威注音輸入法的語言模組總成套裝 威注音輸入法的語言模組總成套裝,以 LMAssembly 命名空間承載下述唯二對外物件:
- vChewingLM總命名空間也承載一些在套裝內共用的工具函式。
- LMConsolidator自動格式整理模組。 - LMConsolidator自動格式整理模組。
- LMInstantiator語言模組副本化模組。另有其日期時間擴充模組可用對 CIN 磁帶模式無效)。 - LMInstantiator語言模組副本化模組亦集成一些自身功能擴展。
LMAssembly 總命名空間也承載一些在套裝內共用的工具函式。
以下是子模組: 以下是子模組:
- lmCassette專門用來處理 CIN 磁帶檔案的模組,命名為「遠野」引擎。
- LMAssociates關聯詞語模組。 - LMAssociates關聯詞語模組。
- lmCassette專門用來處理 CIN 磁帶檔案的模組,命名為「遠野」引擎。
- LMCoreEX可以直接讀取 TXT 格式的帶有權重資料的語彙檔案的模組。 - LMCoreEX可以直接讀取 TXT 格式的帶有權重資料的語彙檔案的模組。
- LMCoreJSON專門用來讀取原廠 JSON 檔案的模組。
- lmPlainBopomofo專門用來讀取使用者自訂ㄅ半候選字順序覆蓋定義檔案plist的模組。 - lmPlainBopomofo專門用來讀取使用者自訂ㄅ半候選字順序覆蓋定義檔案plist的模組。
- lmReplacements專門用來讀取使用者語彙置換模式的辭典資料的模組。 - lmReplacements專門用來讀取使用者語彙置換模式的辭典資料的模組。
- lmUserOverride半衰記憶模組。 - lmUserOverride半衰記憶模組。

View File

@ -11,29 +11,31 @@ import Foundation
/// InputToken.parse Token /// InputToken.parse Token
/// Token .translated() /// Token .translated()
public enum InputToken { extension LMAssembly {
case timeZone(shortened: Bool) enum InputToken {
case timeNow(shortened: Bool) case timeZone(shortened: Bool)
case date(dayDelta: Int = 0, yearDelta: Int = 0, shortened: Bool = true, luna: Bool = false) case timeNow(shortened: Bool)
case week(dayDelta: Int = 0, shortened: Bool = true) case date(dayDelta: Int = 0, yearDelta: Int = 0, shortened: Bool = true, luna: Bool = false)
case year(yearDelta: Int = 0) case week(dayDelta: Int = 0, shortened: Bool = true)
case yearGanzhi(yearDelta: Int = 0) case year(yearDelta: Int = 0)
case yearZodiac(yearDelta: Int = 0) case yearGanzhi(yearDelta: Int = 0)
case yearZodiac(yearDelta: Int = 0)
}
} }
// MARK: - 使 API // MARK: - 使 API
public extension String { public extension String {
func parseAsInputToken(isCHS: Bool) -> [String] { func parseAsInputToken(isCHS: Bool) -> [String] {
InputToken.parse(from: self).map { $0.translated(isCHS: isCHS) }.flatMap { $0 }.deduplicated LMAssembly.InputToken.parse(from: self).map { $0.translated(isCHS: isCHS) }.flatMap { $0 }.deduplicated
} }
} }
// MARK: - Parser parsing raw token value to construct token. // MARK: - Parser parsing raw token value to construct token.
public extension InputToken { extension LMAssembly.InputToken {
static func parse(from rawToken: String) -> [InputToken] { static func parse(from rawToken: String) -> [LMAssembly.InputToken] {
var result: [InputToken] = [] var result: [LMAssembly.InputToken] = []
guard rawToken.prefix(6) == "MACRO@" else { return result } guard rawToken.prefix(6) == "MACRO@" else { return result }
var mapParams: [String: Int] = [:] var mapParams: [String: Int] = [:]
let tokenComponents = rawToken.dropFirst(6).split(separator: "_").map { param in let tokenComponents = rawToken.dropFirst(6).split(separator: "_").map { param in
@ -69,7 +71,7 @@ public extension InputToken {
// MARK: - Parser parsing token itself. // MARK: - Parser parsing token itself.
public extension InputToken { extension LMAssembly.InputToken {
func translated(isCHS: Bool) -> [String] { func translated(isCHS: Bool) -> [String] {
let locale = Locale(identifier: isCHS ? "zh-Hans" : "zh-Hant-TW") let locale = Locale(identifier: isCHS ? "zh-Hans" : "zh-Hant-TW")
let formatter = DateFormatter() let formatter = DateFormatter()

View File

@ -8,9 +8,8 @@
import Foundation import Foundation
import LineReader import LineReader
import Shared
public extension vChewingLM { public extension LMAssembly {
enum LMConsolidator { enum LMConsolidator {
public static let kPragmaHeader = "# 𝙵𝙾𝚁𝙼𝙰𝚃 𝚘𝚛𝚐.𝚊𝚝𝚎𝚕𝚒𝚎𝚛𝙸𝚗𝚖𝚞.𝚟𝚌𝚑𝚎𝚠𝚒𝚗𝚐.𝚞𝚜𝚎𝚛𝙻𝚊𝚗𝚐𝚞𝚊𝚐𝚎𝙼𝚘𝚍𝚎𝚕𝙳𝚊𝚝𝚊.𝚏𝚘𝚛𝚖𝚊𝚝𝚝𝚎𝚍" public static let kPragmaHeader = "# 𝙵𝙾𝚁𝙼𝙰𝚃 𝚘𝚛𝚐.𝚊𝚝𝚎𝚕𝚒𝚎𝚛𝙸𝚗𝚖𝚞.𝚟𝚌𝚑𝚎𝚠𝚒𝚗𝚐.𝚞𝚜𝚎𝚛𝙻𝚊𝚗𝚐𝚞𝚊𝚐𝚎𝙼𝚘𝚍𝚎𝚕𝙳𝚊𝚝𝚊.𝚏𝚘𝚛𝚖𝚊𝚝𝚝𝚎𝚍"
@ -26,19 +25,19 @@ public extension vChewingLM {
let lineReader = try LineReader(file: fileHandle) let lineReader = try LineReader(file: fileHandle)
for strLine in lineReader { // i=0 for strLine in lineReader { // i=0
if strLine != kPragmaHeader { if strLine != kPragmaHeader {
vCLog("Header Mismatch, Starting In-Place Consolidation.") vCLMLog("Header Mismatch, Starting In-Place Consolidation.")
return false return false
} else { } else {
vCLog("Header Verification Succeeded: \(strLine).") vCLMLog("Header Verification Succeeded: \(strLine).")
return true return true
} }
} }
} catch { } catch {
vCLog("Header Verification Failed: File Access Error.") vCLMLog("Header Verification Failed: File Access Error.")
return false return false
} }
} }
vCLog("Header Verification Failed: File Missing.") vCLMLog("Header Verification Failed: File Missing.")
return false return false
} }
@ -51,12 +50,12 @@ public extension vChewingLM {
let dict = try FileManager.default.attributesOfItem(atPath: path) let dict = try FileManager.default.attributesOfItem(atPath: path)
if let value = dict[FileAttributeKey.size] as? UInt64 { fileSize = value } if let value = dict[FileAttributeKey.size] as? UInt64 { fileSize = value }
} catch { } catch {
vCLog("EOF Fix Failed: File Missing at \(path).") vCLMLog("EOF Fix Failed: File Missing at \(path).")
return false return false
} }
guard let fileSize = fileSize else { return false } guard let fileSize = fileSize else { return false }
guard let writeFile = FileHandle(forUpdatingAtPath: path) else { guard let writeFile = FileHandle(forUpdatingAtPath: path) else {
vCLog("EOF Fix Failed: File Not Writable at \(path).") vCLMLog("EOF Fix Failed: File Not Writable at \(path).")
return false return false
} }
defer { writeFile.closeFile() } defer { writeFile.closeFile() }
@ -64,11 +63,11 @@ public extension vChewingLM {
/// consolidate() /// consolidate()
writeFile.seek(toFileOffset: fileSize - 1) writeFile.seek(toFileOffset: fileSize - 1)
if writeFile.readDataToEndOfFile().first != 0x0A { if writeFile.readDataToEndOfFile().first != 0x0A {
vCLog("EOF Missing Confirmed, Start Fixing.") vCLMLog("EOF Missing Confirmed, Start Fixing.")
var newData = Data() var newData = Data()
newData.append(0x0A) newData.append(0x0A)
writeFile.write(newData) writeFile.write(newData)
vCLog("EOF Successfully Assured.") vCLMLog("EOF Successfully Assured.")
} }
return false return false
} }
@ -142,14 +141,29 @@ public extension vChewingLM {
// Write consolidated file contents. // Write consolidated file contents.
try strProcessed.write(to: urlPath, atomically: false, encoding: .utf8) try strProcessed.write(to: urlPath, atomically: false, encoding: .utf8)
} catch { } catch {
vCLog("Consolidation Failed w/ File: \(path), error: \(error)") vCLMLog("Consolidation Failed w/ File: \(path), error: \(error)")
return false return false
} }
vCLog("Either Consolidation Successful Or No-Need-To-Consolidate.") vCLMLog("Either Consolidation Successful Or No-Need-To-Consolidate.")
return true return true
} }
vCLog("Consolidation Failed: File Missing at \(path).") vCLMLog("Consolidation Failed: File Missing at \(path).")
return false return false
} }
} }
} }
private extension String {
mutating func regReplace(pattern: String, replaceWith: String = "") {
// Ref: https://stackoverflow.com/a/40993403/4162914 && https://stackoverflow.com/a/71291137/4162914
do {
let regex = try NSRegularExpression(
pattern: pattern, options: [.caseInsensitive, .anchorsMatchLines]
)
let range = NSRange(startIndex..., in: self)
self = regex.stringByReplacingMatches(
in: self, options: [], range: range, withTemplate: replaceWith
)
} catch { return }
}
}

View File

@ -8,10 +8,8 @@
import Foundation import Foundation
import Megrez import Megrez
import Shared
import SQLite3
public extension vChewingLM { public extension LMAssembly {
/// LMInstantiatorLMI /// LMInstantiatorLMI
/// LangModelProtocol 使 /// LangModelProtocol 使
/// ///
@ -43,42 +41,36 @@ public extension vChewingLM {
public var deltaOfCalendarYears: Int = -2000 public var deltaOfCalendarYears: Int = -2000
} }
public static var asyncLoadingUserData: Bool = true
// SQLite // SQLite
static var ptrSQL: OpaquePointer? static var ptrSQL: OpaquePointer?
// SQLite // SQLite
public private(set) static var isSQLDBConnected: Bool = false public internal(set) static var isSQLDBConnected: Bool = false
// //
public let isCHS: Bool public let isCHS: Bool
// //
public var config = Config() public private(set) var config = Config()
// package // package
public init(isCHS: Bool = false) { public init(
isCHS: Bool = false,
uomDataURL: URL? = nil
) {
self.isCHS = isCHS self.isCHS = isCHS
lmUserOverride = .init(dataURL: uomDataURL)
} }
public func setOptions(handler: (inout Config) -> Void) { @discardableResult public func setOptions(handler: (inout Config) -> Void) -> LMInstantiator {
handler(&config) handler(&config)
return self
} }
@discardableResult public static func connectSQLDB(dbPath: String, dropPreviousConnection: Bool = true) -> Bool { public static func setCassetCandidateKeyValidator(_ validator: @escaping (String) -> Bool) {
if dropPreviousConnection { disconnectSQLDB() } Self.lmCassette.candidateKeysValidator = validator
vCLog("Establishing SQLite connection to: \(dbPath)")
guard sqlite3_open(dbPath, &Self.ptrSQL) == SQLITE_OK else { return false }
guard "PRAGMA journal_mode = OFF;".runAsSQLExec(dbPointer: &ptrSQL) else { return false }
isSQLDBConnected = true
return true
}
public static func disconnectSQLDB() {
if Self.ptrSQL != nil {
sqlite3_close_v2(Self.ptrSQL)
Self.ptrSQL = nil
}
isSQLDBConnected = false
} }
/// ///
@ -93,6 +85,7 @@ public extension vChewingLM {
// currentCassetteMetadata // currentCassetteMetadata
static var lmCassette = LMCassette() static var lmCassette = LMCassette()
static var lmPlainBopomofo = LMPlainBopomofo()
// 使 // 使
// 使使 // 使使
@ -107,30 +100,46 @@ public extension vChewingLM {
) )
var lmReplacements = LMReplacements() var lmReplacements = LMReplacements()
var lmAssociates = LMAssociates() var lmAssociates = LMAssociates()
var lmPlainBopomofo = LMPlainBopomofo()
//
var lmUserOverride: LMUserOverride
// MARK: - // MARK: -
public func resetFactoryJSONModels() {} public func resetFactoryJSONModels() {}
public func loadUserPhrasesData(path: String, filterPath: String?) { public func loadUserPhrasesData(path: String, filterPath: String?) {
DispatchQueue.main.async { func loadMain() {
if FileManager.default.isReadableFile(atPath: path) { if FileManager.default.isReadableFile(atPath: path) {
self.lmUserPhrases.clear() lmUserPhrases.clear()
self.lmUserPhrases.open(path) lmUserPhrases.open(path)
vCLog("lmUserPhrases: \(self.lmUserPhrases.count) entries of data loaded from: \(path)") vCLMLog("lmUserPhrases: \(lmUserPhrases.count) entries of data loaded from: \(path)")
} else { } else {
vCLog("lmUserPhrases: File access failure: \(path)") vCLMLog("lmUserPhrases: File access failure: \(path)")
}
}
if !Self.asyncLoadingUserData {
loadMain()
} else {
DispatchQueue.main.async {
loadMain()
} }
} }
guard let filterPath = filterPath else { return } guard let filterPath = filterPath else { return }
DispatchQueue.main.async { func loadFilter() {
if FileManager.default.isReadableFile(atPath: filterPath) { if FileManager.default.isReadableFile(atPath: filterPath) {
self.lmFiltered.clear() lmFiltered.clear()
self.lmFiltered.open(filterPath) lmFiltered.open(filterPath)
vCLog("lmFiltered: \(self.lmFiltered.count) entries of data loaded from: \(path)") vCLMLog("lmFiltered: \(lmFiltered.count) entries of data loaded from: \(path)")
} else { } else {
vCLog("lmFiltered: File access failure: \(path)") vCLMLog("lmFiltered: File access failure: \(path)")
}
}
if !Self.asyncLoadingUserData {
loadFilter()
} else {
DispatchQueue.main.async {
loadFilter()
} }
} }
} }
@ -140,74 +149,85 @@ public extension vChewingLM {
if FileManager.default.isReadableFile(atPath: path) { if FileManager.default.isReadableFile(atPath: path) {
lmFiltered.clear() lmFiltered.clear()
lmFiltered.open(path) lmFiltered.open(path)
vCLog("lmFiltered: \(lmFiltered.count) entries of data loaded from: \(path)") vCLMLog("lmFiltered: \(lmFiltered.count) entries of data loaded from: \(path)")
} else { } else {
vCLog("lmFiltered: File access failure: \(path)") vCLMLog("lmFiltered: File access failure: \(path)")
} }
} }
public func loadUserSymbolData(path: String) { public func loadUserSymbolData(path: String) {
DispatchQueue.main.async { func load() {
if FileManager.default.isReadableFile(atPath: path) { if FileManager.default.isReadableFile(atPath: path) {
self.lmUserSymbols.clear() lmUserSymbols.clear()
self.lmUserSymbols.open(path) lmUserSymbols.open(path)
vCLog("lmUserSymbol: \(self.lmUserSymbols.count) entries of data loaded from: \(path)") vCLMLog("lmUserSymbol: \(lmUserSymbols.count) entries of data loaded from: \(path)")
} else { } else {
vCLog("lmUserSymbol: File access failure: \(path)") vCLMLog("lmUserSymbol: File access failure: \(path)")
}
}
if !Self.asyncLoadingUserData {
load()
} else {
DispatchQueue.main.async {
load()
} }
} }
} }
public func loadUserAssociatesData(path: String) { public func loadUserAssociatesData(path: String) {
DispatchQueue.main.async { func load() {
if FileManager.default.isReadableFile(atPath: path) { if FileManager.default.isReadableFile(atPath: path) {
self.lmAssociates.clear() lmAssociates.clear()
self.lmAssociates.open(path) lmAssociates.open(path)
vCLog("lmAssociates: \(self.lmAssociates.count) entries of data loaded from: \(path)") vCLMLog("lmAssociates: \(lmAssociates.count) entries of data loaded from: \(path)")
} else { } else {
vCLog("lmAssociates: File access failure: \(path)") vCLMLog("lmAssociates: File access failure: \(path)")
}
}
if !Self.asyncLoadingUserData {
load()
} else {
DispatchQueue.main.async {
load()
} }
} }
} }
public func loadReplacementsData(path: String) { public func loadReplacementsData(path: String) {
DispatchQueue.main.async { func load() {
if FileManager.default.isReadableFile(atPath: path) { if FileManager.default.isReadableFile(atPath: path) {
self.lmReplacements.clear() lmReplacements.clear()
self.lmReplacements.open(path) lmReplacements.open(path)
vCLog("lmReplacements: \(self.lmReplacements.count) entries of data loaded from: \(path)") vCLMLog("lmReplacements: \(lmReplacements.count) entries of data loaded from: \(path)")
} else { } else {
vCLog("lmReplacements: File access failure: \(path)") vCLMLog("lmReplacements: File access failure: \(path)")
} }
} }
} if !Self.asyncLoadingUserData {
load()
public func loadSCPCSequencesData() { } else {
let fileName = !isCHS ? "sequenceDataFromEtenDOS-cht" : "sequenceDataFromEtenDOS-chs" DispatchQueue.main.async {
guard let path = Bundle.module.path(forResource: fileName, ofType: "json") else { load()
vCLog("lmPlainBopomofo: File name access failure: \(fileName)")
return
}
DispatchQueue.main.async {
if FileManager.default.isReadableFile(atPath: path) {
self.lmPlainBopomofo.clear()
self.lmPlainBopomofo.open(path)
vCLog("lmPlainBopomofo: \(self.lmPlainBopomofo.count) entries of data loaded from: \(path)")
} else {
vCLog("lmPlainBopomofo: File access failure: \(path)")
} }
} }
} }
public var isCassetteDataLoaded: Bool { Self.lmCassette.isLoaded } public var isCassetteDataLoaded: Bool { Self.lmCassette.isLoaded }
public static func loadCassetteData(path: String) { public static func loadCassetteData(path: String) {
DispatchQueue.main.async { func load() {
if FileManager.default.isReadableFile(atPath: path) { if FileManager.default.isReadableFile(atPath: path) {
Self.lmCassette.clear() Self.lmCassette.clear()
Self.lmCassette.open(path) Self.lmCassette.open(path)
vCLog("lmCassette: \(Self.lmCassette.count) entries of data loaded from: \(path)") vCLMLog("lmCassette: \(Self.lmCassette.count) entries of data loaded from: \(path)")
} else { } else {
vCLog("lmCassette: File access failure: \(path)") vCLMLog("lmCassette: File access failure: \(path)")
}
}
if !Self.asyncLoadingUserData {
load()
} else {
DispatchQueue.main.async {
load()
} }
} }
} }
@ -333,14 +353,11 @@ public extension vChewingLM {
// 使 // 使
if config.isSCPCEnabled { if config.isSCPCEnabled {
rawAllUnigrams += lmPlainBopomofo.valuesFor(key: keyChain).map { Megrez.Unigram(value: $0, score: 0) } rawAllUnigrams += Self.lmPlainBopomofo.valuesFor(key: keyChain, isCHS: isCHS).map {
Megrez.Unigram(value: $0, score: 0)
}
} }
// reversed 使
//
// rawUserUnigrams
rawAllUnigrams += lmUserPhrases.unigramsFor(key: keyChain).reversed()
if !config.isCassetteEnabled || config.isCassetteEnabled && keyChain.map(\.description)[0] == "_" { if !config.isCassetteEnabled || config.isCassetteEnabled && keyChain.map(\.description)[0] == "_" {
// NumPad // NumPad
rawAllUnigrams += supplyNumPadUnigrams(key: keyChain) rawAllUnigrams += supplyNumPadUnigrams(key: keyChain)
@ -369,6 +386,21 @@ public extension vChewingLM {
} }
} }
// reversed 使
//
// rawUserUnigrams
var userPhraseUnigrams = Array(lmUserPhrases.unigramsFor(key: keyChain).reversed())
if keyArray.count == 1, let topScore = rawAllUnigrams.map(\.score).max() {
// 使
userPhraseUnigrams = userPhraseUnigrams.map { currentUnigram in
Megrez.Unigram(
value: currentUnigram.value,
score: Swift.min(topScore + 0.000_114_514, currentUnigram.score)
)
}
}
rawAllUnigrams = userPhraseUnigrams + rawAllUnigrams
// InputToken // InputToken
rawAllUnigrams = rawAllUnigrams.map { unigram in rawAllUnigrams = rawAllUnigrams.map { unigram in
let convertedValues = unigram.value.parseAsInputToken(isCHS: isCHS) let convertedValues = unigram.value.parseAsInputToken(isCHS: isCHS)

View File

@ -8,9 +8,9 @@
import Foundation import Foundation
import Megrez import Megrez
import Shared import SwiftExtension
public extension vChewingLM.LMInstantiator { public extension LMAssembly.LMInstantiator {
/// ///
var cassetteWildcardKey: String { Self.lmCassette.wildcardKey } var cassetteWildcardKey: String { Self.lmCassette.wildcardKey }
/// ///

View File

@ -11,7 +11,7 @@ import Megrez
// MARK: - 便 // MARK: - 便
extension vChewingLM.LMInstantiator { extension LMAssembly.LMInstantiator {
func queryDateTimeUnigrams(with key: String = "") -> [Megrez.Unigram] { func queryDateTimeUnigrams(with key: String = "") -> [Megrez.Unigram] {
guard let tokenTrigger = TokenTrigger(rawValue: key) else { return [] } guard let tokenTrigger = TokenTrigger(rawValue: key) else { return [] }
var results = [Megrez.Unigram]() var results = [Megrez.Unigram]()

View File

@ -9,7 +9,7 @@
import Foundation import Foundation
import Megrez import Megrez
public extension vChewingLM.LMInstantiator { public extension LMAssembly.LMInstantiator {
func supplyNumPadUnigrams(key: String) -> [Megrez.Unigram] { func supplyNumPadUnigrams(key: String) -> [Megrez.Unigram] {
guard let status = config.numPadFWHWStatus else { return [] } guard let status = config.numPadFWHWStatus else { return [] }
let initials = "_NumPad_" let initials = "_NumPad_"

View File

@ -8,7 +8,6 @@
import Foundation import Foundation
import Megrez import Megrez
import Shared
import SQLite3 import SQLite3
/* ============== /* ==============
@ -31,30 +30,49 @@ import SQLite3
) WITHOUT ROWID; ) WITHOUT ROWID;
*/ */
enum CoreColumn: Int32 { extension LMAssembly.LMInstantiator {
case theDataCHS = 1 // enum CoreColumn: Int32 {
case theDataCHT = 2 // case theDataCHS = 1 //
case theDataCNS = 3 // case theDataCHT = 2 //
case theDataMISC = 4 // case theDataCNS = 3 //
case theDataSYMB = 5 // case theDataMISC = 4 //
case theDataCHEW = 6 // case theDataSYMB = 5 //
case theDataCHEW = 6 //
var name: String { String(describing: self) } var name: String { String(describing: self) }
var id: Int32 { rawValue } var id: Int32 { rawValue }
var defaultScore: Double { var defaultScore: Double {
switch self { switch self {
case .theDataCHEW: return -1 case .theDataCHEW: return -1
case .theDataCNS: return -11 case .theDataCNS: return -11
case .theDataSYMB: return -13 case .theDataSYMB: return -13
case .theDataMISC: return -10 case .theDataMISC: return -10
default: return -9.9 default: return -9.9
}
} }
} }
} }
extension vChewingLM.LMInstantiator { extension LMAssembly.LMInstantiator {
@discardableResult public static func connectSQLDB(dbPath: String, dropPreviousConnection: Bool = true) -> Bool {
if dropPreviousConnection { disconnectSQLDB() }
vCLMLog("Establishing SQLite connection to: \(dbPath)")
guard sqlite3_open(dbPath, &Self.ptrSQL) == SQLITE_OK else { return false }
guard "PRAGMA journal_mode = OFF;".runAsSQLExec(dbPointer: &ptrSQL) else { return false }
isSQLDBConnected = true
return true
}
public static func disconnectSQLDB() {
if Self.ptrSQL != nil {
sqlite3_close_v2(Self.ptrSQL)
Self.ptrSQL = nil
}
isSQLDBConnected = false
}
fileprivate static func querySQL(strStmt sqlQuery: String, coreColumn column: CoreColumn, handler: (String) -> Void) { fileprivate static func querySQL(strStmt sqlQuery: String, coreColumn column: CoreColumn, handler: (String) -> Void) {
guard Self.ptrSQL != nil else { return } guard Self.ptrSQL != nil else { return }
performStatementSansResult { ptrStatement in performStatementSansResult { ptrStatement in
@ -123,9 +141,10 @@ extension vChewingLM.LMInstantiator {
} }
/// UTF8 /// UTF8
/// - Remark: 使
/// - parameters: /// - parameters:
/// - key: /// - key:
func factoryCoreUnigramsFor(key: String) -> [Megrez.Unigram] { public func factoryCoreUnigramsFor(key: String) -> [Megrez.Unigram] {
// ASCII SQLite // ASCII SQLite
factoryUnigramsFor(key: key, column: isCHS ? .theDataCHS : .theDataCHT) factoryUnigramsFor(key: key, column: isCHS ? .theDataCHS : .theDataCHT)
} }
@ -134,7 +153,9 @@ extension vChewingLM.LMInstantiator {
/// - parameters: /// - parameters:
/// - key: /// - key:
/// - column: /// - column:
func factoryUnigramsFor(key: String, column: CoreColumn) -> [Megrez.Unigram] { func factoryUnigramsFor(
key: String, column: LMAssembly.LMInstantiator.CoreColumn
) -> [Megrez.Unigram] {
if key == "_punctuation_list" { return [] } if key == "_punctuation_list" { return [] }
var grams: [Megrez.Unigram] = [] var grams: [Megrez.Unigram] = []
var gramsHW: [Megrez.Unigram] = [] var gramsHW: [Megrez.Unigram] = []
@ -142,8 +163,10 @@ extension vChewingLM.LMInstantiator {
let encryptedKey = Self.cnvPhonabetToASCII(key.replacingOccurrences(of: "'", with: "''")) let encryptedKey = Self.cnvPhonabetToASCII(key.replacingOccurrences(of: "'", with: "''"))
let sqlQuery = "SELECT * FROM DATA_MAIN WHERE theKey='\(encryptedKey)';" let sqlQuery = "SELECT * FROM DATA_MAIN WHERE theKey='\(encryptedKey)';"
Self.querySQL(strStmt: sqlQuery, coreColumn: column) { currentResult in Self.querySQL(strStmt: sqlQuery, coreColumn: column) { currentResult in
let arrRangeRecords = currentResult.split(separator: "\t") var i: Double = 0
for strNetaSet in arrRangeRecords { var previousScore: Double?
currentResult.split(separator: "\t").forEach { strNetaSet in
// stable sort
let neta = Array(strNetaSet.trimmingCharacters(in: .newlines).split(separator: " ").reversed()) let neta = Array(strNetaSet.trimmingCharacters(in: .newlines).split(separator: " ").reversed())
let theValue: String = .init(neta[0]) let theValue: String = .init(neta[0])
var theScore = column.defaultScore var theScore = column.defaultScore
@ -153,8 +176,15 @@ extension vChewingLM.LMInstantiator {
if theScore > 0 { if theScore > 0 {
theScore *= -1 // theScore *= -1 //
} }
if previousScore == theScore {
theScore -= i * 0.000_001
i += 1
} else {
previousScore = theScore
i = 0
}
grams.append(Megrez.Unigram(value: theValue, score: theScore)) grams.append(Megrez.Unigram(value: theValue, score: theScore))
if !key.contains("_punctuation") { continue } if !key.contains("_punctuation") { return }
let halfValue = theValue.applyingTransformFW2HW(reverse: false) let halfValue = theValue.applyingTransformFW2HW(reverse: false)
if halfValue != theValue { if halfValue != theValue {
gramsHW.append(Megrez.Unigram(value: halfValue, score: theScore)) gramsHW.append(Megrez.Unigram(value: halfValue, score: theScore))
@ -210,7 +240,7 @@ extension vChewingLM.LMInstantiator {
} }
} }
private extension vChewingLM.LMInstantiator { private extension LMAssembly.LMInstantiator {
/// ///
/// ///
/// 使 json /// 使 json
@ -258,7 +288,7 @@ private extension vChewingLM.LMInstantiator {
] ]
} }
public extension vChewingLM.LMInstantiator { public extension LMAssembly.LMInstantiator {
@discardableResult static func connectToTestSQLDB() -> Bool { @discardableResult static func connectToTestSQLDB() -> Bool {
Self.connectSQLDB(dbPath: #":memory:"#) && sqlTestCoreLMData.runAsSQLExec(dbPointer: &ptrSQL) Self.connectSQLDB(dbPath: #":memory:"#) && sqlTestCoreLMData.runAsSQLExec(dbPointer: &ptrSQL)
} }

View File

@ -0,0 +1,60 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import Foundation
import Megrez
public extension LMAssembly.LMInstantiator {
func performUOMObservation(
walkedBefore: [Megrez.Node],
walkedAfter: [Megrez.Node],
cursor: Int,
timestamp: Double,
saveCallback: (() -> Void)? = nil
) {
lmUserOverride.performObservation(
walkedBefore: walkedBefore,
walkedAfter: walkedAfter,
cursor: cursor,
timestamp: timestamp,
saveCallback: saveCallback
)
}
func fetchUOMSuggestion(
currentWalk: [Megrez.Node],
cursor: Int,
timestamp: Double
) -> LMAssembly.OverrideSuggestion {
lmUserOverride.fetchSuggestion(
currentWalk: currentWalk,
cursor: cursor,
timestamp: timestamp
)
}
func loadUOMData(fromURL fileURL: URL? = nil) {
lmUserOverride.loadData(fromURL: fileURL)
}
func saveUOMData(toURL fileURL: URL? = nil) {
lmUserOverride.saveData(toURL: fileURL)
}
func clearUOMData(withURL fileURL: URL? = nil) {
lmUserOverride.clearData(withURL: fileURL)
}
func bleachSpecifiedUOMSuggestions(targets: [String], saveCallback: (() -> Void)? = nil) {
lmUserOverride.bleachSpecifiedSuggestions(targets: targets, saveCallback: saveCallback)
}
func bleachUOMUnigrams(saveCallback: (() -> Void)? = nil) {
lmUserOverride.bleachUnigrams(saveCallback: saveCallback)
}
}

View File

@ -0,0 +1,33 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import Foundation
public extension LMAssembly {
struct UserDictionarySummarized: Codable {
let isCHS: Bool
let userPhrases: [String: [String]]
let filter: [String: [String]]
let userSymbols: [String: [String]]
let replacements: [String: String]
let associates: [String: [String]]
}
}
public extension LMAssembly.LMInstantiator {
func summarize(all: Bool) -> LMAssembly.UserDictionarySummarized {
LMAssembly.UserDictionarySummarized(
isCHS: isCHS,
userPhrases: lmUserPhrases.dictRepresented,
filter: lmFiltered.dictRepresented,
userSymbols: lmUserSymbols.dictRepresented,
replacements: lmReplacements.dictRepresented,
associates: all ? lmAssociates.dictRepresented : [:]
)
}
}

View File

@ -0,0 +1,123 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import Foundation
// 使 LMAssembly Tekkon
private typealias LengthSortedDictionary = [Int: [String: String]]
private let mapHanyuPinyinToPhonabets: LengthSortedDictionary = {
let parsed = try? JSONDecoder().decode(LengthSortedDictionary.self, from: jsnHanyuPinyinToMPS.data(using: .utf8) ?? Data([]))
return parsed ?? [:]
}()
extension String {
mutating func convertToPhonabets(newToneOne: String = "") {
if isEmpty || contains("_") || !isNotPureAlphanumerical { return }
let lengths = mapHanyuPinyinToPhonabets.keys.sorted().reversed()
lengths.forEach { length in
mapHanyuPinyinToPhonabets[length]?.forEach { key, value in
self = replacingOccurrences(of: key, with: value)
}
}
self = replacingOccurrences(of: " ", with: newToneOne)
}
}
///
private extension String {
var isNotPureAlphanumerical: Bool {
let regex = ".*[^A-Za-z0-9].*"
let testString = NSPredicate(format: "SELF MATCHES %@", regex)
return testString.evaluate(with: self)
}
}
private let jsnHanyuPinyinToMPS = #"""
{
"1":{"1":" ","2":"ˊ","3":"ˇ","4":"ˋ","5":"˙","a":"","e":"","o":"","q":""},
"2":{"ai":"","an":"","ao":"","ba":"ㄅㄚ","bi":"ㄅㄧ","bo":"ㄅㄛ","bu":"ㄅㄨ",
"ca":"ㄘㄚ","ce":"ㄘㄜ","ci":"","cu":"ㄘㄨ","da":"ㄉㄚ","de":"ㄉㄜ","di":"ㄉㄧ",
"du":"ㄉㄨ","eh":"","ei":"","en":"","er":"","fa":"ㄈㄚ","fo":"ㄈㄛ",
"fu":"ㄈㄨ","ga":"ㄍㄚ","ge":"ㄍㄜ","gi":"ㄍㄧ","gu":"ㄍㄨ","ha":"ㄏㄚ","he":"ㄏㄜ",
"hu":"ㄏㄨ","ji":"ㄐㄧ","ju":"ㄐㄩ","ka":"ㄎㄚ","ke":"ㄎㄜ","ku":"ㄎㄨ","la":"ㄌㄚ",
"le":"ㄌㄜ","li":"ㄌㄧ","lo":"ㄌㄛ","lu":"ㄌㄨ","lv":"ㄌㄩ","ma":"ㄇㄚ","me":"ㄇㄜ",
"mi":"ㄇㄧ","mo":"ㄇㄛ","mu":"ㄇㄨ","na":"ㄋㄚ","ne":"ㄋㄜ","ni":"ㄋㄧ","nu":"ㄋㄨ",
"nv":"ㄋㄩ","ou":"","pa":"ㄆㄚ","pi":"ㄆㄧ","po":"ㄆㄛ","pu":"ㄆㄨ","qi":"ㄑㄧ",
"qu":"ㄑㄩ","re":"ㄖㄜ","ri":"","ru":"ㄖㄨ","sa":"ㄙㄚ","se":"ㄙㄜ","si":"",
"su":"ㄙㄨ","ta":"ㄊㄚ","te":"ㄊㄜ","ti":"ㄊㄧ","tu":"ㄊㄨ","wa":"ㄨㄚ","wo":"ㄨㄛ",
"wu":"","xi":"ㄒㄧ","xu":"ㄒㄩ","ya":"ㄧㄚ","ye":"ㄧㄝ","yi":"","yo":"ㄧㄛ",
"yu":"","za":"ㄗㄚ","ze":"ㄗㄜ","zi":"","zu":"ㄗㄨ"},
"3":{"ang":"","bai":"ㄅㄞ","ban":"ㄅㄢ","bao":"ㄅㄠ","bei":"ㄅㄟ","ben":"ㄅㄣ",
"bie":"ㄅㄧㄝ","bin":"ㄅㄧㄣ","cai":"ㄘㄞ","can":"ㄘㄢ","cao":"ㄘㄠ","cei":"ㄘㄟ",
"cen":"ㄘㄣ","cha":"ㄔㄚ","che":"ㄔㄜ","chi":"","chu":"ㄔㄨ","cou":"ㄘㄡ",
"cui":"ㄘㄨㄟ","cun":"ㄘㄨㄣ","cuo":"ㄘㄨㄛ","dai":"ㄉㄞ","dan":"ㄉㄢ","dao":"ㄉㄠ",
"dei":"ㄉㄟ","den":"ㄉㄣ","dia":"ㄉㄧㄚ","die":"ㄉㄧㄝ","diu":"ㄉㄧㄡ","dou":"ㄉㄡ",
"dui":"ㄉㄨㄟ","dun":"ㄉㄨㄣ","duo":"ㄉㄨㄛ","eng":"","fan":"ㄈㄢ","fei":"ㄈㄟ",
"fen":"ㄈㄣ","fou":"ㄈㄡ","gai":"ㄍㄞ","gan":"ㄍㄢ","gao":"ㄍㄠ","gei":"ㄍㄟ",
"gen":"ㄍㄣ","gin":"ㄍㄧㄣ","gou":"ㄍㄡ","gua":"ㄍㄨㄚ","gue":"ㄍㄨㄜ","gui":"ㄍㄨㄟ",
"gun":"ㄍㄨㄣ","guo":"ㄍㄨㄛ","hai":"ㄏㄞ","han":"ㄏㄢ","hao":"ㄏㄠ","hei":"ㄏㄟ",
"hen":"ㄏㄣ","hou":"ㄏㄡ","hua":"ㄏㄨㄚ","hui":"ㄏㄨㄟ","hun":"ㄏㄨㄣ","huo":"ㄏㄨㄛ",
"jia":"ㄐㄧㄚ","jie":"ㄐㄧㄝ","jin":"ㄐㄧㄣ","jiu":"ㄐㄧㄡ","jue":"ㄐㄩㄝ",
"jun":"ㄐㄩㄣ","kai":"ㄎㄞ","kan":"ㄎㄢ","kao":"ㄎㄠ","ken":"ㄎㄣ","kiu":"ㄎㄧㄡ",
"kou":"ㄎㄡ","kua":"ㄎㄨㄚ","kui":"ㄎㄨㄟ","kun":"ㄎㄨㄣ","kuo":"ㄎㄨㄛ","lai":"ㄌㄞ",
"lan":"ㄌㄢ","lao":"ㄌㄠ","lei":"ㄌㄟ","lia":"ㄌㄧㄚ","lie":"ㄌㄧㄝ","lin":"ㄌㄧㄣ",
"liu":"ㄌㄧㄡ","lou":"ㄌㄡ","lun":"ㄌㄨㄣ","luo":"ㄌㄨㄛ","lve":"ㄌㄩㄝ","mai":"ㄇㄞ",
"man":"ㄇㄢ","mao":"ㄇㄠ","mei":"ㄇㄟ","men":"ㄇㄣ","mie":"ㄇㄧㄝ","min":"ㄇㄧㄣ",
"miu":"ㄇㄧㄡ","mou":"ㄇㄡ","nai":"ㄋㄞ","nan":"ㄋㄢ","nao":"ㄋㄠ","nei":"ㄋㄟ",
"nen":"ㄋㄣ","nie":"ㄋㄧㄝ","nin":"ㄋㄧㄣ","niu":"ㄋㄧㄡ","nou":"ㄋㄡ","nui":"ㄋㄨㄟ",
"nun":"ㄋㄨㄣ","nuo":"ㄋㄨㄛ","nve":"ㄋㄩㄝ","pai":"ㄆㄞ","pan":"ㄆㄢ","pao":"ㄆㄠ",
"pei":"ㄆㄟ","pen":"ㄆㄣ","pia":"ㄆㄧㄚ","pie":"ㄆㄧㄝ","pin":"ㄆㄧㄣ","pou":"ㄆㄡ",
"qia":"ㄑㄧㄚ","qie":"ㄑㄧㄝ","qin":"ㄑㄧㄣ","qiu":"ㄑㄧㄡ","que":"ㄑㄩㄝ",
"qun":"ㄑㄩㄣ","ran":"ㄖㄢ","rao":"ㄖㄠ","ren":"ㄖㄣ","rou":"ㄖㄡ","rui":"ㄖㄨㄟ",
"run":"ㄖㄨㄣ","ruo":"ㄖㄨㄛ","sai":"ㄙㄞ","san":"ㄙㄢ","sao":"ㄙㄠ","sei":"ㄙㄟ",
"sen":"ㄙㄣ","sha":"ㄕㄚ","she":"ㄕㄜ","shi":"","shu":"ㄕㄨ","sou":"ㄙㄡ",
"sui":"ㄙㄨㄟ","sun":"ㄙㄨㄣ","suo":"ㄙㄨㄛ","tai":"ㄊㄞ","tan":"ㄊㄢ","tao":"ㄊㄠ",
"tie":"ㄊㄧㄝ","tou":"ㄊㄡ","tui":"ㄊㄨㄟ","tun":"ㄊㄨㄣ","tuo":"ㄊㄨㄛ",
"wai":"ㄨㄞ","wan":"ㄨㄢ","wei":"ㄨㄟ","wen":"ㄨㄣ","xia":"ㄒㄧㄚ","xie":"ㄒㄧㄝ",
"xin":"ㄒㄧㄣ","xiu":"ㄒㄧㄡ","xue":"ㄒㄩㄝ","xun":"ㄒㄩㄣ","yai":"ㄧㄞ",
"yan":"ㄧㄢ","yao":"ㄧㄠ","yin":"ㄧㄣ","you":"ㄧㄡ","yue":"ㄩㄝ","yun":"ㄩㄣ",
"zai":"ㄗㄞ","zan":"ㄗㄢ","zao":"ㄗㄠ","zei":"ㄗㄟ","zen":"ㄗㄣ","zha":"ㄓㄚ",
"zhe":"ㄓㄜ","zhi":"","zhu":"ㄓㄨ","zou":"ㄗㄡ","zui":"ㄗㄨㄟ","zun":"ㄗㄨㄣ",
"zuo":"ㄗㄨㄛ"},
"4":{"bang":"ㄅㄤ","beng":"ㄅㄥ","bian":"ㄅㄧㄢ","biao":"ㄅㄧㄠ","bing":"ㄅㄧㄥ",
"cang":"ㄘㄤ","ceng":"ㄘㄥ","chai":"ㄔㄞ","chan":"ㄔㄢ","chao":"ㄔㄠ","chen":"ㄔㄣ",
"chou":"ㄔㄡ","chua":"ㄔㄨㄚ","chui":"ㄔㄨㄟ","chun":"ㄔㄨㄣ","chuo":"ㄔㄨㄛ",
"cong":"ㄘㄨㄥ","cuan":"ㄘㄨㄢ","dang":"ㄉㄤ","deng":"ㄉㄥ","dian":"ㄉㄧㄢ",
"diao":"ㄉㄧㄠ","ding":"ㄉㄧㄥ","dong":"ㄉㄨㄥ","duan":"ㄉㄨㄢ","fang":"ㄈㄤ",
"feng":"ㄈㄥ","fiao":"ㄈㄧㄠ","fong":"ㄈㄨㄥ","gang":"ㄍㄤ","geng":"ㄍㄥ",
"giao":"ㄍㄧㄠ","gong":"ㄍㄨㄥ","guai":"ㄍㄨㄞ","guan":"ㄍㄨㄢ","hang":"ㄏㄤ",
"heng":"ㄏㄥ","hong":"ㄏㄨㄥ","huai":"ㄏㄨㄞ","huan":"ㄏㄨㄢ","jian":"ㄐㄧㄢ",
"jiao":"ㄐㄧㄠ","jing":"ㄐㄧㄥ","juan":"ㄐㄩㄢ","kang":"ㄎㄤ","keng":"ㄎㄥ",
"kong":"ㄎㄨㄥ","kuai":"ㄎㄨㄞ","kuan":"ㄎㄨㄢ","lang":"ㄌㄤ","leng":"ㄌㄥ",
"lian":"ㄌㄧㄢ","liao":"ㄌㄧㄠ","ling":"ㄌㄧㄥ","long":"ㄌㄨㄥ","luan":"ㄌㄨㄢ",
"lvan":"ㄌㄩㄢ","mang":"ㄇㄤ","meng":"ㄇㄥ","mian":"ㄇㄧㄢ","miao":"ㄇㄧㄠ",
"ming":"ㄇㄧㄥ","nang":"ㄋㄤ","neng":"ㄋㄥ","nian":"ㄋㄧㄢ","niao":"ㄋㄧㄠ",
"ning":"ㄋㄧㄥ","nong":"ㄋㄨㄥ","nuan":"ㄋㄨㄢ","pang":"ㄆㄤ","peng":"ㄆㄥ",
"pian":"ㄆㄧㄢ","piao":"ㄆㄧㄠ","ping":"ㄆㄧㄥ","qian":"ㄑㄧㄢ","qiao":"ㄑㄧㄠ",
"qing":"ㄑㄧㄥ","quan":"ㄑㄩㄢ","rang":"ㄖㄤ","reng":"ㄖㄥ","rong":"ㄖㄨㄥ",
"ruan":"ㄖㄨㄢ","sang":"ㄙㄤ","seng":"ㄙㄥ","shai":"ㄕㄞ","shan":"ㄕㄢ",
"shao":"ㄕㄠ","shei":"ㄕㄟ","shen":"ㄕㄣ","shou":"ㄕㄡ","shua":"ㄕㄨㄚ",
"shui":"ㄕㄨㄟ","shun":"ㄕㄨㄣ","shuo":"ㄕㄨㄛ","song":"ㄙㄨㄥ","suan":"ㄙㄨㄢ",
"tang":"ㄊㄤ","teng":"ㄊㄥ","tian":"ㄊㄧㄢ","tiao":"ㄊㄧㄠ","ting":"ㄊㄧㄥ",
"tong":"ㄊㄨㄥ","tuan":"ㄊㄨㄢ","wang":"ㄨㄤ","weng":"ㄨㄥ","xian":"ㄒㄧㄢ",
"xiao":"ㄒㄧㄠ","xing":"ㄒㄧㄥ","xuan":"ㄒㄩㄢ","yang":"ㄧㄤ","ying":"ㄧㄥ",
"yong":"ㄩㄥ","yuan":"ㄩㄢ","zang":"ㄗㄤ","zeng":"ㄗㄥ","zhai":"ㄓㄞ",
"zhan":"ㄓㄢ","zhao":"ㄓㄠ","zhei":"ㄓㄟ","zhen":"ㄓㄣ","zhou":"ㄓㄡ",
"zhua":"ㄓㄨㄚ","zhui":"ㄓㄨㄟ","zhun":"ㄓㄨㄣ","zhuo":"ㄓㄨㄛ",
"zong":"ㄗㄨㄥ","zuan":"ㄗㄨㄢ"},
"5":{"biang":"ㄅㄧㄤ","chang":"ㄔㄤ","cheng":"ㄔㄥ","chong":"ㄔㄨㄥ","chuai":"ㄔㄨㄞ",
"chuan":"ㄔㄨㄢ","duang":"ㄉㄨㄤ","guang":"ㄍㄨㄤ","huang":"ㄏㄨㄤ","jiang":"ㄐㄧㄤ",
"jiong":"ㄐㄩㄥ","kiang":"ㄎㄧㄤ","kuang":"ㄎㄨㄤ","liang":"ㄌㄧㄤ","niang":"ㄋㄧㄤ",
"qiang":"ㄑㄧㄤ","qiong":"ㄑㄩㄥ","shang":"ㄕㄤ","sheng":"ㄕㄥ","shuai":"ㄕㄨㄞ",
"shuan":"ㄕㄨㄢ","xiang":"ㄒㄧㄤ","xiong":"ㄒㄩㄥ","zhang":"ㄓㄤ","zheng":"ㄓㄥ",
"zhong":"ㄓㄨㄥ","zhuai":"ㄓㄨㄞ","zhuan":"ㄓㄨㄢ"},
"6":{"chuang":"ㄔㄨㄤ","shuang":"ㄕㄨㄤ","zhuang":"ㄓㄨㄤ"}
}
"""#

View File

@ -7,13 +7,11 @@
// requirements defined in MIT License. // requirements defined in MIT License.
import Megrez import Megrez
import PinyinPhonaConverter
import Shared
public extension vChewingLM { extension LMAssembly {
@frozen struct LMAssociates { struct LMAssociates {
public private(set) var filePath: String? public private(set) var filePath: String?
var rangeMap: [String: [(Range<String.Index>, Int)]] = [:] var rangeMap: [String: [(Range<String.Index>, Int)]] = [:] // Range index
var strData: String = "" var strData: String = ""
public var count: Int { rangeMap.count } public var count: Int { rangeMap.count }
@ -48,8 +46,8 @@ public extension vChewingLM {
replaceData(textData: rawStrData) replaceData(textData: rawStrData)
} catch { } catch {
filePath = oldPath filePath = oldPath
vCLog("\(error)") vCLMLog("\(error)")
vCLog("↑ Exception happened when reading data at: \(path).") vCLMLog("↑ Exception happened when reading data at: \(path).")
return false return false
} }
@ -93,28 +91,21 @@ public extension vChewingLM {
do { do {
try strData.write(toFile: filePath, atomically: true, encoding: .utf8) try strData.write(toFile: filePath, atomically: true, encoding: .utf8)
} catch { } catch {
vCLog("Failed to save current database to: \(filePath)") vCLMLog("Failed to save current database to: \(filePath)")
} }
} }
public func valuesFor(pair: Megrez.KeyValuePaired) -> [String] { public func valuesFor(pair: Megrez.KeyValuePaired) -> [String] {
var pairs: [String] = [] var pairs: [String] = []
if let arrRangeRecords: [(Range<String.Index>, Int)] = rangeMap[pair.toNGramKey] { let availableResults = [rangeMap[pair.toNGramKey], rangeMap[pair.value]].compactMap { $0 }
for (netaRange, index) in arrRangeRecords { availableResults.forEach { arrRangeRecords in
arrRangeRecords.forEach { netaRange, index in
let neta = strData[netaRange].split(separator: " ") let neta = strData[netaRange].split(separator: " ")
let theValue: String = .init(neta[index]) let theValue: String = .init(neta[index])
pairs.append(theValue) pairs.append(theValue)
} }
} }
if let arrRangeRecords: [(Range<String.Index>, Int)] = rangeMap[pair.value] { return pairs.deduplicated
for (netaRange, index) in arrRangeRecords {
let neta = strData[netaRange].split(separator: " ")
let theValue: String = .init(neta[index])
pairs.append(theValue)
}
}
var set = Set<String>()
return pairs.filter { set.insert($0).inserted }
} }
public func hasValuesFor(pair: Megrez.KeyValuePaired) -> Bool { public func hasValuesFor(pair: Megrez.KeyValuePaired) -> Bool {
@ -123,3 +114,17 @@ public extension vChewingLM {
} }
} }
} }
extension LMAssembly.LMAssociates {
var dictRepresented: [String: [String]] {
var result = [String: [String]]()
rangeMap.forEach { key, arrRangeRecords in
arrRangeRecords.forEach { netaRange, index in
let neta = strData[netaRange].split(separator: " ")
let theValue: String = .init(neta[index])
result[key, default: []].append(theValue)
}
}
return result
}
}

View File

@ -10,11 +10,10 @@
import Foundation import Foundation
import LineReader import LineReader
import Megrez import Megrez
import Shared
public extension vChewingLM { extension LMAssembly {
/// 便使 /// 便使
@frozen struct LMCassette { struct LMCassette {
public private(set) var filePath: String? public private(set) var filePath: String?
public private(set) var nameShort: String = "" public private(set) var nameShort: String = ""
public private(set) var nameENG: String = "" public private(set) var nameENG: String = ""
@ -40,12 +39,13 @@ public extension vChewingLM {
public private(set) var areCandidateKeysShiftHeld: Bool = false public private(set) var areCandidateKeysShiftHeld: Bool = false
public private(set) var supplyQuickResults: Bool = false public private(set) var supplyQuickResults: Bool = false
public private(set) var supplyPartiallyMatchedResults: Bool = false public private(set) var supplyPartiallyMatchedResults: Bool = false
public var candidateKeysValidator: (String) -> Bool = { _ in false }
/// 西 - NORM /// 西 - NORM
private var norm = 0.0 private var norm = 0.0
} }
} }
public extension vChewingLM.LMCassette { extension LMAssembly.LMCassette {
/// 西 - fscale /// 西 - fscale
private static let fscale = 2.7 private static let fscale = 2.7
/// ///
@ -86,7 +86,7 @@ public extension vChewingLM.LMCassette {
if FileManager.default.fileExists(atPath: path) { if FileManager.default.fileExists(atPath: path) {
do { do {
guard let fileHandle = FileHandle(forReadingAtPath: path) else { guard let fileHandle = FileHandle(forReadingAtPath: path) else {
throw vChewingLM.FileErrors.fileHandleError("") throw LMAssembly.FileErrors.fileHandleError("")
} }
let lineReader = try LineReader(file: fileHandle) let lineReader = try LineReader(file: fileHandle)
var theMaxKeyLength = 1 var theMaxKeyLength = 1
@ -195,7 +195,7 @@ public extension vChewingLM.LMCassette {
// Post process. // Post process.
// Package 便 J / K // Package 便 J / K
// //
if CandidateKey.validate(keys: selectionKeys) != nil { selectionKeys = "1234567890" } if !candidateKeysValidator(selectionKeys) { selectionKeys = "1234567890" }
if !keysUsedInCharDef.intersection(selectionKeys.map(\.description)).isEmpty { if !keysUsedInCharDef.intersection(selectionKeys.map(\.description)).isEmpty {
areCandidateKeysShiftHeld = true areCandidateKeysShiftHeld = true
} }
@ -204,10 +204,10 @@ public extension vChewingLM.LMCassette {
filePath = path filePath = path
return true return true
} catch { } catch {
vCLog("CIN Loading Failed: File Access Error.") vCLMLog("CIN Loading Failed: File Access Error.")
} }
} else { } else {
vCLog("CIN Loading Failed: File Missing.") vCLMLog("CIN Loading Failed: File Missing.")
} }
filePath = oldPath filePath = oldPath
return false return false

View File

@ -7,15 +7,13 @@
// requirements defined in MIT License. // requirements defined in MIT License.
import Megrez import Megrez
import PinyinPhonaConverter
import Shared
public extension vChewingLM { extension LMAssembly {
/// LMCore LMCoreEX range /// LMCore LMCoreEX range
/// range strData /// range strData
/// C++ ParselessLM Swift /// C++ ParselessLM Swift
/// For /// For
@frozen struct LMCoreEX { struct LMCoreEX {
public private(set) var filePath: String? public private(set) var filePath: String?
/// 便 strData /// 便 strData
var rangeMap: [String: [Range<String.Index>]] = [:] var rangeMap: [String: [Range<String.Index>]] = [:]
@ -81,8 +79,8 @@ public extension vChewingLM {
replaceData(textData: rawStrData) replaceData(textData: rawStrData)
} catch { } catch {
filePath = oldPath filePath = oldPath
vCLog("\(error)") vCLMLog("\(error)")
vCLog("↑ Exception happened when reading data at: \(path).") vCLMLog("↑ Exception happened when reading data at: \(path).")
return false return false
} }
@ -133,7 +131,7 @@ public extension vChewingLM {
} }
try dataToWrite.write(toFile: filePath, atomically: true, encoding: .utf8) try dataToWrite.write(toFile: filePath, atomically: true, encoding: .utf8)
} catch { } catch {
vCLog("Failed to save current database to: \(filePath)") vCLMLog("Failed to save current database to: \(filePath)")
} }
} }
@ -150,7 +148,7 @@ public extension vChewingLM {
strDump += addline strDump += addline
} }
} }
vCLog(strDump) vCLMLog(strDump)
} }
/// strData /// strData
@ -186,3 +184,15 @@ public extension vChewingLM {
} }
} }
} }
extension LMAssembly.LMCoreEX {
var dictRepresented: [String: [String]] {
var result = [String: [String]]()
rangeMap.forEach { key, arrValueRanges in
result[key, default: []] = arrValueRanges.map { currentRange in
strData[currentRange].description
}
}
return result
}
}

View File

@ -7,67 +7,36 @@
// requirements defined in MIT License. // requirements defined in MIT License.
import Foundation import Foundation
import Shared
public extension vChewingLM { extension LMAssembly {
@frozen struct LMPlainBopomofo { struct LMPlainBopomofo {
public private(set) var filePath: String? @usableFromInline typealias DataMap = [String: [String: String]]
var dataMap: [String: String] = [:] let dataMap: DataMap
public var count: Int { dataMap.count } public var count: Int { dataMap.count }
public init() { public init() {
dataMap = [:] do {
let rawData = jsnEtenDosSequence.data(using: .utf8) ?? .init([])
let rawJSON = try JSONDecoder().decode([String: [String: String]].self, from: rawData)
dataMap = rawJSON
} catch {
vCLMLog("\(error)")
vCLMLog("↑ Exception happened when parsing raw JSON sequence data from vChewing LMAssembly.")
dataMap = [:]
}
} }
public var isLoaded: Bool { !dataMap.isEmpty } public var isLoaded: Bool { !dataMap.isEmpty }
@discardableResult public mutating func open(_ path: String) -> Bool { public func valuesFor(key: String, isCHS: Bool) -> [String] {
if isLoaded { return false }
let oldPath = filePath
filePath = nil
do {
let rawData = try Data(contentsOf: URL(fileURLWithPath: path))
if let rawJSON = try? JSONSerialization.jsonObject(with: rawData) as? [String: String] {
dataMap = rawJSON
} else {
filePath = oldPath
vCLog("↑ Exception happened when reading JSON file at: \(path).")
return false
}
} catch {
filePath = oldPath
vCLog("\(error)")
vCLog("↑ Exception happened when reading JSON file at: \(path).")
return false
}
filePath = path
return true
}
public mutating func clear() {
filePath = nil
dataMap.removeAll()
}
public func saveData() {
guard let filePath = filePath, let plistURL = URL(string: filePath) else { return }
do {
let plistData = try PropertyListSerialization.data(fromPropertyList: dataMap, format: .binary, options: 0)
try plistData.write(to: plistURL)
} catch {
vCLog("Failed to save current database to: \(filePath)")
}
}
public func valuesFor(key: String) -> [String] {
var pairs: [String] = [] var pairs: [String] = []
if let arrRangeRecords: String = dataMap[key]?.trimmingCharacters(in: .newlines) { let subKey = isCHS ? "S" : "T"
if let arrRangeRecords: String = dataMap[key]?[subKey] {
pairs.append(contentsOf: arrRangeRecords.map(\.description)) pairs.append(contentsOf: arrRangeRecords.map(\.description))
} }
return pairs.deduplicated //
return pairs
} }
public func hasValuesFor(key: String) -> Bool { dataMap.keys.contains(key) } public func hasValuesFor(key: String) -> Bool { dataMap.keys.contains(key) }

View File

@ -6,10 +6,8 @@
// marks, or product names of Contributor, except as required to fulfill notice // marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License. // requirements defined in MIT License.
import Shared extension LMAssembly {
struct LMReplacements {
public extension vChewingLM {
@frozen struct LMReplacements {
public private(set) var filePath: String? public private(set) var filePath: String?
var rangeMap: [String: Range<String.Index>] = [:] var rangeMap: [String: Range<String.Index>] = [:]
var strData: String = "" var strData: String = ""
@ -35,8 +33,8 @@ public extension vChewingLM {
replaceData(textData: rawStrData) replaceData(textData: rawStrData)
} catch { } catch {
filePath = oldPath filePath = oldPath
vCLog("\(error)") vCLMLog("\(error)")
vCLog("↑ Exception happened when reading data at: \(path).") vCLMLog("↑ Exception happened when reading data at: \(path).")
return false return false
} }
@ -72,7 +70,7 @@ public extension vChewingLM {
do { do {
try strData.write(toFile: filePath, atomically: true, encoding: .utf8) try strData.write(toFile: filePath, atomically: true, encoding: .utf8)
} catch { } catch {
vCLog("Failed to save current database to: \(filePath)") vCLMLog("Failed to save current database to: \(filePath)")
} }
} }
@ -81,7 +79,7 @@ public extension vChewingLM {
for entry in rangeMap { for entry in rangeMap {
strDump += strData[entry.value] + "\n" strDump += strData[entry.value] + "\n"
} }
vCLog(strDump) vCLMLog(strDump)
} }
public func valuesFor(key: String) -> String { public func valuesFor(key: String) -> String {
@ -100,3 +98,13 @@ public extension vChewingLM {
} }
} }
} }
extension LMAssembly.LMReplacements {
var dictRepresented: [String: String] {
var result = [String: String]()
rangeMap.forEach { key, valueRange in
result[key] = strData[valueRange].description
}
return result
}
}

View File

@ -1,76 +0,0 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import Foundation
import Shared
public extension vChewingLM {
@frozen struct LMRevLookup {
public private(set) var dataMap: [String: [String]] = [:]
public private(set) var filePath: String = ""
public init(data dictData: (dict: [String: [String]]?, path: String)) {
guard let theDict = dictData.dict else {
vCLog("↑ Exception happened when reading JSON file at: \(dictData.path).")
return
}
filePath = dictData.path
dataMap = theDict
}
public init(path: String) {
if path.isEmpty { return }
do {
let rawData = try Data(contentsOf: URL(fileURLWithPath: path))
if let rawJSON = try? JSONSerialization.jsonObject(with: rawData) as? [String: [String]] {
dataMap = rawJSON
} else {
vCLog("↑ Exception happened when reading JSON file at: \(path).")
return
}
} catch {
vCLog("↑ Exception happened when reading JSON file at: \(path).")
return
}
filePath = path
}
public func query(with kanji: String) -> [String]? {
guard let resultData = dataMap[kanji] else { return nil }
let resultArray = resultData.compactMap {
let result = restorePhonabetFromASCII($0)
return result.isEmpty ? nil : result
}
return resultArray.isEmpty ? nil : resultArray
}
///
///
/// ASCII
/// - parameters:
/// - incoming:
func restorePhonabetFromASCII(_ incoming: String) -> String {
var strOutput = incoming
if !strOutput.contains("_") {
for entry in Self.dicPhonabet4ASCII {
strOutput = strOutput.replacingOccurrences(of: entry.key, with: entry.value)
}
}
return strOutput
}
// MARK: - Constants
static let dicPhonabet4ASCII: [String: String] = [
"b": "", "p": "", "m": "", "f": "", "d": "", "t": "", "n": "", "l": "", "g": "", "k": "", "h": "",
"j": "", "q": "", "x": "", "Z": "", "C": "", "S": "", "r": "", "z": "", "c": "", "s": "", "i": "",
"u": "", "v": "", "a": "", "o": "", "e": "", "E": "", "B": "", "P": "", "M": "", "F": "", "D": "",
"T": "", "N": "", "L": "", "R": "", "2": "ˊ", "3": "ˇ", "4": "ˋ", "5": "˙",
]
}
}

View File

@ -9,74 +9,41 @@
import Foundation import Foundation
import Megrez import Megrez
import Shared
public extension vChewingLM { // MARK: - Public Types.
public extension LMAssembly {
struct OverrideSuggestion {
public var candidates = [(String, Megrez.Unigram)]()
public var forceHighScoreOverride = false
public var isEmpty: Bool { candidates.isEmpty }
}
}
// MARK: - LMUserOverride Class Definition.
extension LMAssembly {
class LMUserOverride { class LMUserOverride {
// MARK: - Main
var mutCapacity: Int var mutCapacity: Int
var mutDecayExponent: Double var mutDecayExponent: Double
var mutLRUList: [KeyObservationPair] = [] var mutLRUList: [KeyObservationPair] = []
var mutLRUMap: [String: KeyObservationPair] = [:] var mutLRUMap: [String: KeyObservationPair] = [:]
let kDecayThreshold: Double = 1.0 / 1_048_576.0 // let kDecayThreshold: Double = 1.0 / 1_048_576.0 //
var fileSaveLocationURL: URL var fileSaveLocationURL: URL?
public static let kObservedOverrideHalfLife: Double = 3600.0 * 6 // 6 public static let kObservedOverrideHalfLife: Double = 3600.0 * 6 // 6
public init(capacity: Int = 500, decayConstant: Double = LMUserOverride.kObservedOverrideHalfLife, dataURL: URL) { public init(capacity: Int = 500, decayConstant: Double = LMUserOverride.kObservedOverrideHalfLife, dataURL: URL? = nil) {
mutCapacity = max(capacity, 1) // Ensures that this integer value is always > 0. mutCapacity = max(capacity, 1) // Ensures that this integer value is always > 0.
mutDecayExponent = log(0.5) / decayConstant mutDecayExponent = log(0.5) / decayConstant
fileSaveLocationURL = dataURL fileSaveLocationURL = dataURL
} }
public func performObservation(
walkedBefore: [Megrez.Node], walkedAfter: [Megrez.Node],
cursor: Int, timestamp: Double, saveCallback: @escaping () -> Void
) {
//
guard !walkedAfter.isEmpty, !walkedBefore.isEmpty else { return }
guard walkedBefore.totalKeyCount == walkedAfter.totalKeyCount else { return }
//
var actualCursor = 0
guard let currentNode = walkedAfter.findNode(at: cursor, target: &actualCursor) else { return }
// 使
guard currentNode.spanLength <= 3 else { return }
//
guard actualCursor > 0 else { return } //
let currentNodeIndex = actualCursor
actualCursor -= 1
var prevNodeIndex = 0
guard let prevNode = walkedBefore.findNode(at: actualCursor, target: &prevNodeIndex) else { return }
let forceHighScoreOverride: Bool = currentNode.spanLength > prevNode.spanLength
let breakingUp = currentNode.spanLength == 1 && prevNode.spanLength > 1
let targetNodeIndex = breakingUp ? currentNodeIndex : prevNodeIndex
let key: String = vChewingLM.LMUserOverride.formObservationKey(
walkedNodes: walkedAfter, headIndex: targetNodeIndex
)
guard !key.isEmpty else { return }
doObservation(
key: key, candidate: currentNode.currentUnigram.value, timestamp: timestamp,
forceHighScoreOverride: forceHighScoreOverride, saveCallback: { saveCallback() }
)
}
public func fetchSuggestion(
currentWalk: [Megrez.Node], cursor: Int, timestamp: Double
) -> Suggestion {
var headIndex = 0
guard let nodeIter = currentWalk.findNode(at: cursor, target: &headIndex) else { return .init() }
let key = vChewingLM.LMUserOverride.formObservationKey(walkedNodes: currentWalk, headIndex: headIndex)
return getSuggestion(key: key, timestamp: timestamp, headReading: nodeIter.joinedKey())
}
} }
} }
// MARK: - Private Structures // MARK: - Private Structures
extension vChewingLM.LMUserOverride { extension LMAssembly.LMUserOverride {
enum OverrideUnit: CodingKey { case count, timestamp, forceHighScoreOverride } enum OverrideUnit: CodingKey { case count, timestamp, forceHighScoreOverride }
enum ObservationUnit: CodingKey { case count, overrides } enum ObservationUnit: CodingKey { case count, overrides }
enum KeyObservationPairUnit: CodingKey { case key, observation } enum KeyObservationPairUnit: CodingKey { case key, observation }
@ -153,10 +120,52 @@ extension vChewingLM.LMUserOverride {
} }
} }
// MARK: - Hash and Dehash the entire UOM data, etc. // MARK: - Internal Methods in LMAssembly.
public extension vChewingLM.LMUserOverride { extension LMAssembly.LMUserOverride {
func bleachSpecifiedSuggestions(targets: [String], saveCallback: @escaping () -> Void) { func performObservation(
walkedBefore: [Megrez.Node], walkedAfter: [Megrez.Node],
cursor: Int, timestamp: Double, saveCallback: (() -> Void)? = nil
) {
//
guard !walkedAfter.isEmpty, !walkedBefore.isEmpty else { return }
guard walkedBefore.totalKeyCount == walkedAfter.totalKeyCount else { return }
//
var actualCursor = 0
guard let currentNode = walkedAfter.findNode(at: cursor, target: &actualCursor) else { return }
// 使
guard currentNode.spanLength <= 3 else { return }
//
guard actualCursor > 0 else { return } //
let currentNodeIndex = actualCursor
actualCursor -= 1
var prevNodeIndex = 0
guard let prevNode = walkedBefore.findNode(at: actualCursor, target: &prevNodeIndex) else { return }
let forceHighScoreOverride: Bool = currentNode.spanLength > prevNode.spanLength
let breakingUp = currentNode.spanLength == 1 && prevNode.spanLength > 1
let targetNodeIndex = breakingUp ? currentNodeIndex : prevNodeIndex
let key: String = LMAssembly.LMUserOverride.formObservationKey(
walkedNodes: walkedAfter, headIndex: targetNodeIndex
)
guard !key.isEmpty else { return }
doObservation(
key: key, candidate: currentNode.currentUnigram.value, timestamp: timestamp,
forceHighScoreOverride: forceHighScoreOverride, saveCallback: saveCallback
)
}
func fetchSuggestion(
currentWalk: [Megrez.Node], cursor: Int, timestamp: Double
) -> LMAssembly.OverrideSuggestion {
var headIndex = 0
guard let nodeIter = currentWalk.findNode(at: cursor, target: &headIndex) else { return .init() }
let key = LMAssembly.LMUserOverride.formObservationKey(walkedNodes: currentWalk, headIndex: headIndex)
return getSuggestion(key: key, timestamp: timestamp, headReading: nodeIter.joinedKey())
}
func bleachSpecifiedSuggestions(targets: [String], saveCallback: (() -> Void)? = nil) {
if targets.isEmpty { return } if targets.isEmpty { return }
for neta in mutLRUMap { for neta in mutLRUMap {
for target in targets { for target in targets {
@ -166,82 +175,86 @@ public extension vChewingLM.LMUserOverride {
} }
} }
resetMRUList() resetMRUList()
saveCallback() saveCallback?() ?? saveData()
} }
/// LRU /// LRU
func bleachUnigrams(saveCallback: @escaping () -> Void) { func bleachUnigrams(saveCallback: (() -> Void)? = nil) {
for key in mutLRUMap.keys { for key in mutLRUMap.keys {
if !key.contains("(),()") { continue } if !key.contains("(),()") { continue }
mutLRUMap.removeValue(forKey: key) mutLRUMap.removeValue(forKey: key)
} }
resetMRUList() resetMRUList()
saveCallback() saveCallback?() ?? saveData()
} }
internal func resetMRUList() { func resetMRUList() {
mutLRUList.removeAll() mutLRUList.removeAll()
for neta in mutLRUMap.reversed() { for neta in mutLRUMap.reversed() {
mutLRUList.append(neta.value) mutLRUList.append(neta.value)
} }
} }
func clearData(withURL fileURL: URL) { func clearData(withURL fileURL: URL? = nil) {
mutLRUMap = .init() mutLRUMap = .init()
mutLRUList = .init() mutLRUList = .init()
do { do {
let nullData = "{}" let nullData = "{}"
guard let fileURL = fileURL ?? fileSaveLocationURL else {
throw UOMError(rawValue: "given fileURL is invalid or nil.")
}
try nullData.write(to: fileURL, atomically: false, encoding: .utf8) try nullData.write(to: fileURL, atomically: false, encoding: .utf8)
} catch { } catch {
vCLog("UOM Error: Unable to clear data. Details: \(error)") vCLMLog("UOM Error: Unable to clear the data in the UOM file. Details: \(error)")
return return
} }
} }
func saveData(toURL fileURL: URL? = nil) { func saveData(toURL fileURL: URL? = nil) {
guard let fileURL: URL = fileURL ?? fileSaveLocationURL else {
vCLMLog("UOM saveData() failed. At least the file Save URL is not set for the current UOM.")
return
}
// 使 JSONSerialization // 使 JSONSerialization
let encoder = JSONEncoder() let encoder = JSONEncoder()
do { do {
guard let jsonData = try? encoder.encode(mutLRUMap) else { return } guard let jsonData = try? encoder.encode(mutLRUMap) else { return }
let fileURL: URL = fileURL ?? fileSaveLocationURL
try jsonData.write(to: fileURL, options: .atomic) try jsonData.write(to: fileURL, options: .atomic)
} catch { } catch {
vCLog("UOM Error: Unable to save data, abort saving. Details: \(error)") vCLMLog("UOM Error: Unable to save data, abort saving. Details: \(error)")
return return
} }
} }
func loadData(fromURL fileURL: URL) { func loadData(fromURL fileURL: URL? = nil) {
guard let fileURL: URL = fileURL ?? fileSaveLocationURL else {
vCLMLog("UOM loadData() failed. At least the file Load URL is not set for the current UOM.")
return
}
// 使 JSONSerialization // 使 JSONSerialization
let decoder = JSONDecoder() let decoder = JSONDecoder()
do { do {
let data = try Data(contentsOf: fileURL, options: .mappedIfSafe) let data = try Data(contentsOf: fileURL, options: .mappedIfSafe)
if ["", "{}"].contains(String(data: data, encoding: .utf8)) { return } if ["", "{}"].contains(String(data: data, encoding: .utf8)) { return }
guard let jsonResult = try? decoder.decode([String: KeyObservationPair].self, from: data) else { guard let jsonResult = try? decoder.decode([String: KeyObservationPair].self, from: data) else {
vCLog("UOM Error: Read file content type invalid, abort loading.") vCLMLog("UOM Error: Read file content type invalid, abort loading.")
return return
} }
mutLRUMap = jsonResult mutLRUMap = jsonResult
resetMRUList() resetMRUList()
} catch { } catch {
vCLog("UOM Error: Unable to read file or parse the data, abort loading. Details: \(error)") vCLMLog("UOM Error: Unable to read file or parse the data, abort loading. Details: \(error)")
return return
} }
} }
struct Suggestion {
public var candidates = [(String, Megrez.Unigram)]()
public var forceHighScoreOverride = false
public var isEmpty: Bool { candidates.isEmpty }
}
} }
// MARK: - Private Methods // MARK: - Other Non-Public Internal Methods
extension vChewingLM.LMUserOverride { extension LMAssembly.LMUserOverride {
func doObservation( func doObservation(
key: String, candidate: String, timestamp: Double, forceHighScoreOverride: Bool, key: String, candidate: String, timestamp: Double, forceHighScoreOverride: Bool,
saveCallback: @escaping () -> Void saveCallback: (() -> Void)?
) { ) {
guard mutLRUMap[key] != nil else { guard mutLRUMap[key] != nil else {
var observation: Observation = .init() var observation: Observation = .init()
@ -257,8 +270,8 @@ extension vChewingLM.LMUserOverride {
mutLRUMap.removeValue(forKey: mutLRUList[mutLRUList.endIndex - 1].key) mutLRUMap.removeValue(forKey: mutLRUList[mutLRUList.endIndex - 1].key)
mutLRUList.removeLast() mutLRUList.removeLast()
} }
vCLog("UOM: Observation finished with new observation: \(key)") vCLMLog("UOM: Observation finished with new observation: \(key)")
saveCallback() saveCallback?() ?? saveData()
return return
} }
// decayCallback // decayCallback
@ -268,12 +281,12 @@ extension vChewingLM.LMUserOverride {
) )
mutLRUList.insert(theNeta, at: 0) mutLRUList.insert(theNeta, at: 0)
mutLRUMap[key] = theNeta mutLRUMap[key] = theNeta
vCLog("UOM: Observation finished with existing observation: \(key)") vCLMLog("UOM: Observation finished with existing observation: \(key)")
saveCallback() saveCallback?() ?? saveData()
} }
} }
func getSuggestion(key: String, timestamp: Double, headReading: String) -> Suggestion { func getSuggestion(key: String, timestamp: Double, headReading: String) -> LMAssembly.OverrideSuggestion {
guard !key.isEmpty, let kvPair = mutLRUMap[key] else { return .init() } guard !key.isEmpty, let kvPair = mutLRUMap[key] else { return .init() }
let observation: Observation = kvPair.observation let observation: Observation = kvPair.observation
var candidates: [(String, Megrez.Unigram)] = .init() var candidates: [(String, Megrez.Unigram)] = .init()
@ -386,3 +399,10 @@ extension vChewingLM.LMUserOverride {
return result return result
} }
} }
struct UOMError: LocalizedError {
var rawValue: String
var errorDescription: String? {
NSLocalizedString("rawValue", comment: "")
}
}

View File

@ -1,9 +1,5 @@
// // libTaBE (http://sourceforge.net/projects/libtabe/)
// File.swift // (2002 ). 1999 Pai-Hsiang Hsiao BSD
//
//
// Created by ShikiSuen on 2023/11/26.
//
import Foundation import Foundation

View File

@ -7,10 +7,9 @@
// requirements defined in MIT License. // requirements defined in MIT License.
import Foundation import Foundation
import Shared
import SQLite3 import SQLite3
public enum vChewingLM { public enum LMAssembly {
enum FileErrors: Error { enum FileErrors: Error {
case fileHandleError(String) case fileHandleError(String)
} }
@ -56,7 +55,7 @@ extension Array where Element == String {
sqlite3_prepare_v2(ptrDB, strStmt, -1, &ptrStmt, nil) == SQLITE_OK && sqlite3_step(ptrStmt) == SQLITE_DONE sqlite3_prepare_v2(ptrDB, strStmt, -1, &ptrStmt, nil) == SQLITE_OK && sqlite3_step(ptrStmt) == SQLITE_DONE
} }
guard thisResult else { guard thisResult else {
vCLog("SQL Query Error. Statement: \(strStmt)") vCLMLog("SQL Query Error. Statement: \(strStmt)")
return false return false
} }
} }
@ -83,3 +82,13 @@ func performStatementSansResult(_ handler: (inout OpaquePointer?) -> Void) {
} }
handler(&ptrStmt) handler(&ptrStmt)
} }
func vCLMLog(_ strPrint: StringLiteralType) {
guard let toLog = UserDefaults.standard.object(forKey: "_DebugMode") as? Bool else {
NSLog("vChewingDebug: %@", strPrint)
return
}
if toLog {
NSLog("vChewingDebug: %@", strPrint)
}
}

View File

@ -57,8 +57,8 @@ final class InputTokenTests: XCTestCase {
} }
func testGeneratedResultsFromLMInstantiator() throws { func testGeneratedResultsFromLMInstantiator() throws {
let instance = vChewingLM.LMInstantiator(isCHS: true) let instance = LMAssembly.LMInstantiator(isCHS: true)
XCTAssertTrue(vChewingLM.LMInstantiator.connectToTestSQLDB()) XCTAssertTrue(LMAssembly.LMInstantiator.connectToTestSQLDB())
instance.setOptions { config in instance.setOptions { config in
config.isCNSEnabled = false config.isCNSEnabled = false
config.isSymbolEnabled = false config.isSymbolEnabled = false
@ -70,6 +70,6 @@ final class InputTokenTests: XCTestCase {
) )
let x = instance.unigramsFor(keyArray: ["ㄐㄧㄣ", "ㄊㄧㄢ", "ㄖˋ", "ㄑㄧˊ"]).description let x = instance.unigramsFor(keyArray: ["ㄐㄧㄣ", "ㄊㄧㄢ", "ㄖˋ", "ㄑㄧˊ"]).description
print(x) print(x)
vChewingLM.LMInstantiator.disconnectSQLDB() LMAssembly.LMInstantiator.disconnectSQLDB()
} }
} }

View File

@ -20,7 +20,7 @@ private let testDataPath: String = packageRootPath + "/Tests/TestCINData/"
final class LMCassetteTests: XCTestCase { final class LMCassetteTests: XCTestCase {
func testCassetteLoadWubi86() throws { func testCassetteLoadWubi86() throws {
let pathCINFile = testDataPath + "wubi.cin" let pathCINFile = testDataPath + "wubi.cin"
var lmCassette = vChewingLM.LMCassette() var lmCassette = LMAssembly.LMCassette()
NSLog("LMCassette: Start loading CIN.") NSLog("LMCassette: Start loading CIN.")
lmCassette.open(pathCINFile) lmCassette.open(pathCINFile)
NSLog("LMCassette: Finished loading CIN. Entries: \(lmCassette.count)") NSLog("LMCassette: Finished loading CIN. Entries: \(lmCassette.count)")
@ -41,7 +41,7 @@ final class LMCassetteTests: XCTestCase {
func testCassetteLoadArray30() throws { func testCassetteLoadArray30() throws {
let pathCINFile = testDataPath + "array30.cin2" let pathCINFile = testDataPath + "array30.cin2"
var lmCassette = vChewingLM.LMCassette() var lmCassette = LMAssembly.LMCassette()
NSLog("LMCassette: Start loading CIN.") NSLog("LMCassette: Start loading CIN.")
lmCassette.open(pathCINFile) lmCassette.open(pathCINFile)
NSLog("LMCassette: Finished loading CIN. Entries: \(lmCassette.count)") NSLog("LMCassette: Finished loading CIN. Entries: \(lmCassette.count)")

View File

@ -38,7 +38,7 @@ private let sampleData: String = #"""
final class LMCoreEXTests: XCTestCase { final class LMCoreEXTests: XCTestCase {
func testLMCoreEXAsFactoryCoreDict() throws { func testLMCoreEXAsFactoryCoreDict() throws {
var lmTest = vChewingLM.LMCoreEX( var lmTest = LMAssembly.LMCoreEX(
reverse: false, consolidate: false, defaultScore: 0, forceDefaultScore: false reverse: false, consolidate: false, defaultScore: 0, forceDefaultScore: false
) )
lmTest.replaceData(textData: sampleData) lmTest.replaceData(textData: sampleData)

View File

@ -22,8 +22,8 @@ private let expectedReverseLookupResults: [String] = [
final class LMInstantiatorSQLTests: XCTestCase { final class LMInstantiatorSQLTests: XCTestCase {
func testSQL() throws { func testSQL() throws {
let instance = vChewingLM.LMInstantiator(isCHS: true) let instance = LMAssembly.LMInstantiator(isCHS: true)
XCTAssertTrue(vChewingLM.LMInstantiator.connectToTestSQLDB()) XCTAssertTrue(LMAssembly.LMInstantiator.connectToTestSQLDB())
instance.setOptions { config in instance.setOptions { config in
config.isCNSEnabled = false config.isCNSEnabled = false
config.isSymbolEnabled = false config.isSymbolEnabled = false
@ -41,13 +41,13 @@ final class LMInstantiatorSQLTests: XCTestCase {
XCTAssertEqual(instance.unigramsFor(keyArray: strRefutationKey).count, 10) XCTAssertEqual(instance.unigramsFor(keyArray: strRefutationKey).count, 10)
XCTAssertEqual(instance.unigramsFor(keyArray: strBoobsKey).last?.description, "(☉☉,-13.0)") XCTAssertEqual(instance.unigramsFor(keyArray: strBoobsKey).last?.description, "(☉☉,-13.0)")
// //
XCTAssertEqual(vChewingLM.LMInstantiator.getFactoryReverseLookupData(with: ""), expectedReverseLookupResults) XCTAssertEqual(LMAssembly.LMInstantiator.getFactoryReverseLookupData(with: ""), expectedReverseLookupResults)
vChewingLM.LMInstantiator.disconnectSQLDB() LMAssembly.LMInstantiator.disconnectSQLDB()
} }
func testCNSMask() throws { func testCNSMask() throws {
let instance = vChewingLM.LMInstantiator(isCHS: false) let instance = LMAssembly.LMInstantiator(isCHS: false)
XCTAssertTrue(vChewingLM.LMInstantiator.connectToTestSQLDB()) XCTAssertTrue(LMAssembly.LMInstantiator.connectToTestSQLDB())
instance.setOptions { config in instance.setOptions { config in
config.isCNSEnabled = false config.isCNSEnabled = false
config.isSymbolEnabled = false config.isSymbolEnabled = false

View File

@ -0,0 +1,36 @@
//// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// StringView Ranges extension by (c) 2022 and onwards Isaac Xen (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import Foundation
import XCTest
@testable import LangModelAssembly
final class LMPlainBPMFTests: XCTestCase {
func testLMPlainBPMFDataQuery() throws {
let instance1 = LMAssembly.LMInstantiator(isCHS: false).setOptions { config in
config.isSCPCEnabled = true
}
var liu2 = instance1.unigramsFor(keyArray: ["ㄌㄧㄡˊ"]).map(\.value).prefix(3)
var bao3 = instance1.unigramsFor(keyArray: ["ㄅㄠˇ"]).map(\.value).prefix(3)
var jie2 = instance1.unigramsFor(keyArray: ["ㄐㄧㄝˊ"]).map(\.value).prefix(3)
XCTAssertEqual(liu2, ["", "", ""])
XCTAssertEqual(bao3, ["", "", ""])
XCTAssertEqual(jie2, ["", "", ""])
let instance2 = LMAssembly.LMInstantiator(isCHS: true).setOptions { config in
config.isSCPCEnabled = true
}
liu2 = instance2.unigramsFor(keyArray: ["ㄌㄧㄡˊ"]).map(\.value).prefix(3)
bao3 = instance2.unigramsFor(keyArray: ["ㄅㄠˇ"]).map(\.value).prefix(3)
jie2 = instance2.unigramsFor(keyArray: ["ㄐㄧㄝˊ"]).map(\.value).prefix(3)
XCTAssertEqual(liu2, ["", "", ""])
XCTAssertEqual(bao3, ["", "", ""])
XCTAssertEqual(jie2, ["", "", ""])
}
}

View File

@ -17,12 +17,12 @@ private let halfLife: Double = 5400
private let nullURL = URL(fileURLWithPath: "/dev/null") private let nullURL = URL(fileURLWithPath: "/dev/null")
final class LMUserOverrideTests: XCTestCase { final class LMUserOverrideTests: XCTestCase {
private func observe(who uom: vChewingLM.LMUserOverride, key: String, candidate: String, timestamp stamp: Double) { private func observe(who uom: LMAssembly.LMUserOverride, key: String, candidate: String, timestamp stamp: Double) {
uom.doObservation(key: key, candidate: candidate, timestamp: stamp, forceHighScoreOverride: false, saveCallback: {}) uom.doObservation(key: key, candidate: candidate, timestamp: stamp, forceHighScoreOverride: false, saveCallback: {})
} }
func testUOM_1_BasicOps() throws { func testUOM_1_BasicOps() throws {
let uom = vChewingLM.LMUserOverride(capacity: capacity, decayConstant: Double(halfLife), dataURL: nullURL) let uom = LMAssembly.LMUserOverride(capacity: capacity, decayConstant: Double(halfLife), dataURL: nullURL)
let key = "((ㄕㄣˊ-ㄌㄧˇ-ㄌㄧㄥˊ-ㄏㄨㄚˊ,神里綾華),(ㄉㄜ˙,的),ㄍㄡˇ)" let key = "((ㄕㄣˊ-ㄌㄧˇ-ㄌㄧㄥˊ-ㄏㄨㄚˊ,神里綾華),(ㄉㄜ˙,的),ㄍㄡˇ)"
let headReading = "ㄍㄡˇ" let headReading = "ㄍㄡˇ"
let expectedSuggestion = "" let expectedSuggestion = ""
@ -45,7 +45,7 @@ final class LMUserOverrideTests: XCTestCase {
} }
func testUOM_2_NewestAgainstRepeatedlyUsed() throws { func testUOM_2_NewestAgainstRepeatedlyUsed() throws {
let uom = vChewingLM.LMUserOverride(capacity: capacity, decayConstant: Double(halfLife), dataURL: nullURL) let uom = LMAssembly.LMUserOverride(capacity: capacity, decayConstant: Double(halfLife), dataURL: nullURL)
let key = "((ㄕㄣˊ-ㄌㄧˇ-ㄌㄧㄥˊ-ㄏㄨㄚˊ,神里綾華),(ㄉㄜ˙,的),ㄍㄡˇ)" let key = "((ㄕㄣˊ-ㄌㄧˇ-ㄌㄧㄥˊ-ㄏㄨㄚˊ,神里綾華),(ㄉㄜ˙,的),ㄍㄡˇ)"
let headReading = "ㄍㄡˇ" let headReading = "ㄍㄡˇ"
let valRepeatedlyUsed = "" // let valRepeatedlyUsed = "" //
@ -74,7 +74,7 @@ final class LMUserOverrideTests: XCTestCase {
let b = (key: "((ㄆㄞˋ-ㄇㄥˊ,派蒙),(ㄉㄜ˙,的),ㄐㄧㄤˇ-ㄐㄧㄣ)", value: "伙食費", head: "ㄏㄨㄛˇ-ㄕˊ-ㄈㄟˋ") let b = (key: "((ㄆㄞˋ-ㄇㄥˊ,派蒙),(ㄉㄜ˙,的),ㄐㄧㄤˇ-ㄐㄧㄣ)", value: "伙食費", head: "ㄏㄨㄛˇ-ㄕˊ-ㄈㄟˋ")
let c = (key: "((ㄍㄨㄛˊ-ㄅㄥ,國崩),(ㄉㄜ˙,的),ㄇㄠˋ-ㄗ˙)", value: "帽子", head: "ㄇㄠˋ-ㄗ˙") let c = (key: "((ㄍㄨㄛˊ-ㄅㄥ,國崩),(ㄉㄜ˙,的),ㄇㄠˋ-ㄗ˙)", value: "帽子", head: "ㄇㄠˋ-ㄗ˙")
let d = (key: "((ㄌㄟˊ-ㄉㄧㄢˋ-ㄐㄧㄤ-ㄐㄩㄣ,雷電將軍),(ㄉㄜ˙,的),ㄐㄧㄠˇ-ㄔㄡˋ)", value: "腳臭", head: "ㄐㄧㄠˇ-ㄔㄡˋ") let d = (key: "((ㄌㄟˊ-ㄉㄧㄢˋ-ㄐㄧㄤ-ㄐㄩㄣ,雷電將軍),(ㄉㄜ˙,的),ㄐㄧㄠˇ-ㄔㄡˋ)", value: "腳臭", head: "ㄐㄧㄠˇ-ㄔㄡˋ")
let uom = vChewingLM.LMUserOverride(capacity: 2, decayConstant: Double(halfLife), dataURL: nullURL) let uom = LMAssembly.LMUserOverride(capacity: 2, decayConstant: Double(halfLife), dataURL: nullURL)
observe(who: uom, key: a.key, candidate: a.value, timestamp: nowTimeStamp) observe(who: uom, key: a.key, candidate: a.value, timestamp: nowTimeStamp)
observe(who: uom, key: b.key, candidate: b.value, timestamp: nowTimeStamp + halfLife * 1) observe(who: uom, key: b.key, candidate: b.value, timestamp: nowTimeStamp + halfLife * 1)
observe(who: uom, key: c.key, candidate: c.value, timestamp: nowTimeStamp + halfLife * 2) observe(who: uom, key: c.key, candidate: c.value, timestamp: nowTimeStamp + halfLife * 2)

View File

@ -13,7 +13,7 @@ import XCTest
final class LMInstantiatorNumericPadTests: XCTestCase { final class LMInstantiatorNumericPadTests: XCTestCase {
func testSQL() throws { func testSQL() throws {
let instance = vChewingLM.LMInstantiator(isCHS: true) let instance = LMAssembly.LMInstantiator(isCHS: true)
instance.setOptions { config in instance.setOptions { config in
config.numPadFWHWStatus = nil config.numPadFWHWStatus = nil
} }

View File

@ -17,8 +17,9 @@ let package = Package(
.package(path: "../HangarRash_SwiftyCapsLockToggler"), .package(path: "../HangarRash_SwiftyCapsLockToggler"),
.package(path: "../Jad_BookmarkManager"), .package(path: "../Jad_BookmarkManager"),
.package(path: "../Qwertyyb_ShiftKeyUpChecker"), .package(path: "../Qwertyyb_ShiftKeyUpChecker"),
.package(path: "../vChewing_BrailleSputnik"),
.package(path: "../vChewing_CandidateWindow"), .package(path: "../vChewing_CandidateWindow"),
.package(path: "../vChewing_CocoaExtension"), .package(path: "../vChewing_OSFrameworkImpl"),
.package(path: "../vChewing_Hotenka"), .package(path: "../vChewing_Hotenka"),
.package(path: "../vChewing_IMKUtils"), .package(path: "../vChewing_IMKUtils"),
.package(path: "../vChewing_KimoDataReader"), .package(path: "../vChewing_KimoDataReader"),
@ -38,9 +39,10 @@ let package = Package(
.target( .target(
name: "MainAssembly", name: "MainAssembly",
dependencies: [ dependencies: [
.product(name: "BrailleSputnik", package: "vChewing_BrailleSputnik"),
.product(name: "BookmarkManager", package: "Jad_BookmarkManager"), .product(name: "BookmarkManager", package: "Jad_BookmarkManager"),
.product(name: "CandidateWindow", package: "vChewing_CandidateWindow"), .product(name: "CandidateWindow", package: "vChewing_CandidateWindow"),
.product(name: "CocoaExtension", package: "vChewing_CocoaExtension"), .product(name: "OSFrameworkImpl", package: "vChewing_OSFrameworkImpl"),
.product(name: "FolderMonitor", package: "DanielGalasko_FolderMonitor"), .product(name: "FolderMonitor", package: "DanielGalasko_FolderMonitor"),
.product(name: "Hotenka", package: "vChewing_Hotenka"), .product(name: "Hotenka", package: "vChewing_Hotenka"),
.product(name: "IMKUtils", package: "vChewing_IMKUtils"), .product(name: "IMKUtils", package: "vChewing_IMKUtils"),

View File

@ -7,6 +7,7 @@
// requirements defined in MIT License. // requirements defined in MIT License.
import AppKit import AppKit
import Shared
import SwiftUI import SwiftUI
public class CtlAboutUI: NSWindowController, NSWindowDelegate { public class CtlAboutUI: NSWindowController, NSWindowDelegate {

View File

@ -7,6 +7,7 @@
// requirements defined in MIT License. // requirements defined in MIT License.
import AppKit import AppKit
import Shared
import SwiftUI import SwiftUI
public struct VwrAboutUI { public struct VwrAboutUI {

View File

@ -69,6 +69,8 @@ public extension AppDelegate {
SpeechSputnik.shared.refreshStatus() // SpeechSputnik.shared.refreshStatus() //
CandidateTextService.enableFinalSanityCheck()
// 使 // 使
// Debug // Debug
// Debug // Debug
@ -145,7 +147,7 @@ public extension AppDelegate {
guard let currentMemorySizeInBytes = NSApplication.memoryFootprint else { return 0 } guard let currentMemorySizeInBytes = NSApplication.memoryFootprint else { return 0 }
let currentMemorySize: Double = (Double(currentMemorySizeInBytes) / 1024 / 1024).rounded(toPlaces: 1) let currentMemorySize: Double = (Double(currentMemorySizeInBytes) / 1024 / 1024).rounded(toPlaces: 1)
switch currentMemorySize { switch currentMemorySize {
case 384...: case 1024...:
vCLog("WARNING: EXCESSIVE MEMORY FOOTPRINT (\(currentMemorySize)MB).") vCLog("WARNING: EXCESSIVE MEMORY FOOTPRINT (\(currentMemorySize)MB).")
let msgPackage = UNMutableNotificationContent() let msgPackage = UNMutableNotificationContent()
msgPackage.title = NSLocalizedString("vChewing", comment: "") msgPackage.title = NSLocalizedString("vChewing", comment: "")
@ -167,4 +169,10 @@ public extension AppDelegate {
} }
return currentMemorySize return currentMemorySize
} }
// New About Window
@IBAction func about(_: Any) {
CtlAboutUI.show()
NSApp.popup()
}
} }

View File

@ -0,0 +1,170 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import BrailleSputnik
import Foundation
import Shared
import Tekkon
public extension CandidateTextService {
// MARK: - Final Sanity Check Implementation.
static func enableFinalSanityCheck() {
finalSanityCheck = finalSanityCheckImplemented
}
private static func finalSanityCheckImplemented(_ target: CandidateTextService) -> Bool {
switch target.value {
case .url: return true
case let .selector(strSelector):
guard target.candidateText != "%s" else { return true } //
switch strSelector {
case "copyUnicodeMetadata:": return true
case _ where strSelector.hasPrefix("copyRuby"),
_ where strSelector.hasPrefix("copyBraille"),
_ where strSelector.hasPrefix("copyInline"):
return !target.reading.joined().isEmpty // 便 [""]
default: return true
}
}
}
// MARK: - Selector Methods, CandidatePairServicable, and the Coordinator.
var responseFromSelector: String? {
switch value {
case .url: return nil
case let .selector(string):
let passable = CandidatePairServicable(value: candidateText, reading: reading)
return Coordinator().runTask(selectorName: string, candidate: passable)
}
}
@objcMembers class CandidatePairServicable: NSObject {
public var value: String
public var reading: [String]
public init(value: String, reading: [String] = []) {
self.value = value
self.reading = reading
}
public typealias SubPair = (key: String, value: String)
@nonobjc var smashed: [SubPair] {
var pairs = [SubPair]()
if value.count != reading.count {
pairs.append((reading.joined(separator: " "), value))
} else {
value.enumerated().forEach { i, valChar in
pairs.append((reading[i], valChar.description))
}
}
return pairs
}
}
@objc class Coordinator: NSObject {
private var result: String?
public func runTask(selectorName: String, candidate param: CandidatePairServicable) -> String? {
guard !selectorName.isEmpty, !param.value.isEmpty else { return nil }
guard responds(to: Selector(selectorName)) else { return nil }
performSelector(onMainThread: Selector(selectorName), with: param, waitUntilDone: true)
defer { result = nil }
return result
}
/// Unicode
/// - Parameter param:
@objc func copyUnicodeMetadata(_ param: CandidatePairServicable) {
var resultArray = [String]()
param.value.forEach { char in
resultArray.append("\(char) \(char.description.charDescriptions.first ?? "NULL")")
}
result = resultArray.joined(separator: "\n")
}
/// HTML Ruby ()
/// - Parameter param:
@objc func copyRubyHTMLZhuyinTextbookStyle(_ param: CandidatePairServicable) {
prepareTextBookZhuyinReadings(param)
copyRubyHTMLCommon(param)
}
/// HTML Ruby ()
/// - Parameter param:
@objc func copyRubyHTMLHanyuPinyinTextbookStyle(_ param: CandidatePairServicable) {
prepareTextBookPinyinReadings(param)
copyRubyHTMLCommon(param)
}
/// ()
/// - Parameter param:
@objc func copyInlineZhuyinAnnotationTextbookStyle(_ param: CandidatePairServicable) {
prepareTextBookZhuyinReadings(param)
copyInlineAnnotationCommon(param)
}
/// ()
/// - Parameter param:
@objc func copyInlineHanyuPinyinAnnotationTextbookStyle(_ param: CandidatePairServicable) {
prepareTextBookPinyinReadings(param)
copyInlineAnnotationCommon(param)
}
@objc func copyBraille1947(_ param: CandidatePairServicable) {
result = BrailleSputnik(standard: .of1947).convertToBraille(smashedPairs: param.smashed)
}
@objc func copyBraille2018(_ param: CandidatePairServicable) {
result = BrailleSputnik(standard: .of2018).convertToBraille(smashedPairs: param.smashed)
}
// MARK: Privates
}
}
private extension CandidateTextService.Coordinator {
func copyInlineAnnotationCommon(_ param: CandidateTextService.CandidatePairServicable) {
var composed = ""
param.smashed.forEach { subPair in
let subKey = subPair.key
let subValue = subPair.value
composed += subKey.contains("_") ? subValue : "\(subValue)(\(subKey))"
}
result = composed
}
func copyRubyHTMLCommon(_ param: CandidateTextService.CandidatePairServicable) {
var composed = ""
param.smashed.forEach { subPair in
let subKey = subPair.key
let subValue = subPair.value
composed += subKey.contains("_") ? subValue : "<ruby>\(subValue)<rp>(</rp><rt>\(subKey)</rt><rp>)</rp></ruby>"
}
result = composed
}
func prepareTextBookZhuyinReadings(_ param: CandidateTextService.CandidatePairServicable) {
let newReadings = param.reading.map { currentReading in
if currentReading.contains("_") { return "_??" }
return Tekkon.cnvPhonaToTextbookStyle(target: currentReading)
}
param.reading = newReadings
}
func prepareTextBookPinyinReadings(_ param: CandidateTextService.CandidatePairServicable) {
let newReadings = param.reading.map { currentReading in
if currentReading.contains("_") { return "_??" }
return Tekkon.cnvHanyuPinyinToTextbookStyle(
targetJoined: Tekkon.cnvPhonaToHanyuPinyin(targetJoined: currentReading)
)
}
param.reading = newReadings
}
}

View File

@ -7,6 +7,7 @@
// requirements defined in MIT License. // requirements defined in MIT License.
import Hotenka import Hotenka
import Shared
public enum ChineseConverter { public enum ChineseConverter {
public static let shared = HotenkaChineseConverter( public static let shared = HotenkaChineseConverter(

View File

@ -9,12 +9,12 @@
import AppKit import AppKit
import Foundation import Foundation
import Shared import Shared
import UniformTypeIdentifiers
public class VwrClientListMgr: NSViewController { public class VwrClientListMgr: NSViewController {
let windowWidth: CGFloat = 770 let windowWidth: CGFloat = 770
let contentWidth: CGFloat = 750 let contentWidth: CGFloat = 750
let buttonWidth: CGFloat = 150 let buttonWidth: CGFloat = 150
let tableHeight: CGFloat = 230
lazy var tblClients: NSTableView = .init() lazy var tblClients: NSTableView = .init()
lazy var btnAddClient = NSButton("Add Client", target: self, action: #selector(btnAddClientClicked(_:))) lazy var btnAddClient = NSButton("Add Client", target: self, action: #selector(btnAddClientClicked(_:)))
@ -35,7 +35,7 @@ public class VwrClientListMgr: NSViewController {
var body: NSView? { var body: NSView? {
NSStackView.build(.vertical, insets: .new(all: 14)) { NSStackView.build(.vertical, insets: .new(all: 14)) {
makeScrollableTable() makeScrollableTable()
.makeSimpleConstraint(.height, relation: .equal, value: 232) .makeSimpleConstraint(.height, relation: .equal, value: tableHeight)
NSStackView.build(.horizontal) { NSStackView.build(.horizontal) {
let descriptionWidth = contentWidth - buttonWidth - 20 let descriptionWidth = contentWidth - buttonWidth - 20
NSStackView.build(.vertical) { NSStackView.build(.vertical) {
@ -59,6 +59,8 @@ public class VwrClientListMgr: NSViewController {
scrollContainer.scrollerStyle = .legacy scrollContainer.scrollerStyle = .legacy
scrollContainer.autohidesScrollers = true scrollContainer.autohidesScrollers = true
scrollContainer.documentView = tblClients scrollContainer.documentView = tblClients
scrollContainer.hasVerticalScroller = true
scrollContainer.hasHorizontalScroller = true
if #available(macOS 11.0, *) { if #available(macOS 11.0, *) {
tblClients.style = .inset tblClients.style = .inset
} }
@ -72,12 +74,12 @@ public class VwrClientListMgr: NSViewController {
tblClients.autosaveTableColumns = false tblClients.autosaveTableColumns = false
tblClients.backgroundColor = NSColor.controlBackgroundColor tblClients.backgroundColor = NSColor.controlBackgroundColor
tblClients.columnAutoresizingStyle = .lastColumnOnlyAutoresizingStyle tblClients.columnAutoresizingStyle = .lastColumnOnlyAutoresizingStyle
tblClients.frame = CGRect(x: 0, y: 0, width: 728, height: 230) tblClients.frame = CGRect(x: 0, y: 0, width: 728, height: tableHeight)
tblClients.gridColor = NSColor.clear tblClients.gridColor = NSColor.clear
tblClients.intercellSpacing = CGSize(width: 17, height: 0) tblClients.intercellSpacing = CGSize(width: 17, height: 0)
tblClients.rowHeight = 24 tblClients.rowHeight = 24
tblClients.setContentHuggingPriority(.defaultHigh, for: .vertical) tblClients.setContentHuggingPriority(.defaultHigh, for: .vertical)
tblClients.registerForDraggedTypes([.init(rawValue: kUTTypeFileURL as String)]) tblClients.registerForDraggedTypes([.kUTTypeFileURL])
tblClients.dataSource = self tblClients.dataSource = self
tblClients.action = #selector(onItemClicked(_:)) tblClients.action = #selector(onItemClicked(_:))
tblClients.target = self tblClients.target = self
@ -150,7 +152,7 @@ extension VwrClientListMgr {
neta info: NSDraggingInfo, onError: @escaping () -> Void?, handler: (([URL]) -> Void)? = nil neta info: NSDraggingInfo, onError: @escaping () -> Void?, handler: (([URL]) -> Void)? = nil
) { ) {
let board = info.draggingPasteboard let board = info.draggingPasteboard
let type = NSPasteboard.PasteboardType(rawValue: kUTTypeApplicationBundle as String) let type = NSPasteboard.PasteboardType.kUTTypeAppBundle
let options: [NSPasteboard.ReadingOptionKey: Any] = [ let options: [NSPasteboard.ReadingOptionKey: Any] = [
.urlReadingFileURLsOnly: true, .urlReadingFileURLsOnly: true,
.urlReadingContentsConformToTypes: [type], .urlReadingContentsConformToTypes: [type],

View File

@ -200,7 +200,8 @@ public extension IMEState {
case .ofCandidates where cursor != marker: return data.attributedStringMarking(for: session) case .ofCandidates where cursor != marker: return data.attributedStringMarking(for: session)
case .ofCandidates where cursor == marker: break case .ofCandidates where cursor == marker: break
case .ofAssociates: return data.attributedStringPlaceholder(for: session) case .ofAssociates: return data.attributedStringPlaceholder(for: session)
case .ofSymbolTable where displayedText.isEmpty: return data.attributedStringPlaceholder(for: session) case .ofSymbolTable where displayedText.isEmpty || node.containsCandidateServices:
return data.attributedStringPlaceholder(for: session)
case .ofSymbolTable where !displayedText.isEmpty: break case .ofSymbolTable where !displayedText.isEmpty: break
default: break default: break
} }

View File

@ -212,7 +212,7 @@ public extension IMEStateData {
subNeta = Tekkon.cnvPhonaToHanyuPinyin(targetJoined: subNeta) subNeta = Tekkon.cnvPhonaToHanyuPinyin(targetJoined: subNeta)
subNeta = Tekkon.cnvHanyuPinyinToTextbookStyle(targetJoined: subNeta) subNeta = Tekkon.cnvHanyuPinyinToTextbookStyle(targetJoined: subNeta)
} else { } else {
subNeta = Tekkon.cnvPhonaToTextbookReading(target: subNeta) subNeta = Tekkon.cnvPhonaToTextbookStyle(target: subNeta)
} }
} }
arrOutput.append(subNeta) arrOutput.append(subNeta)

View File

@ -19,8 +19,7 @@ import Tekkon
// MARK: - InputHandler (Protocol). // MARK: - InputHandler (Protocol).
public protocol InputHandlerProtocol { public protocol InputHandlerProtocol {
var currentLM: vChewingLM.LMInstantiator { get set } var currentLM: LMAssembly.LMInstantiator { get set }
var currentUOM: vChewingLM.LMUserOverride { get set }
var delegate: InputHandlerDelegate? { get set } var delegate: InputHandlerDelegate? { get set }
var keySeparator: String { get } var keySeparator: String { get }
static var keySeparator: String { get } static var keySeparator: String { get }
@ -99,8 +98,7 @@ public class InputHandler: InputHandlerProtocol {
var composer: Tekkon.Composer = .init() // var composer: Tekkon.Composer = .init() //
var compositor: Megrez.Compositor // var compositor: Megrez.Compositor //
public var currentUOM: vChewingLM.LMUserOverride public var currentLM: LMAssembly.LMInstantiator {
public var currentLM: vChewingLM.LMInstantiator {
didSet { didSet {
compositor.langModel = .init(withLM: currentLM) compositor.langModel = .init(withLM: currentLM)
clear() clear()
@ -108,10 +106,9 @@ public class InputHandler: InputHandlerProtocol {
} }
/// ///
public init(lm: vChewingLM.LMInstantiator, uom: vChewingLM.LMUserOverride, pref: PrefMgrProtocol) { public init(lm: LMAssembly.LMInstantiator, pref: PrefMgrProtocol) {
prefs = pref prefs = pref
currentLM = lm currentLM = lm
currentUOM = uom
/// ///
Megrez.Compositor.maxSpanLength = prefs.maxCandidateLength Megrez.Compositor.maxSpanLength = prefs.maxCandidateLength
/// ensureCompositor() /// ensureCompositor()
@ -369,8 +366,8 @@ public class InputHandler: InputHandlerProtocol {
let currentNode = currentWalk.findNode(at: actualNodeCursorPosition, target: &accumulatedCursor) let currentNode = currentWalk.findNode(at: actualNodeCursorPosition, target: &accumulatedCursor)
guard let currentNode = currentNode else { return } guard let currentNode = currentNode else { return }
uom: if currentNode.currentUnigram.score > -12, prefs.fetchSuggestionsFromUserOverrideModel { uomProcessing: if currentNode.currentUnigram.score > -12, prefs.fetchSuggestionsFromUserOverrideModel {
if skipObservation { break uom } if skipObservation { break uomProcessing }
vCLog("UOM: Start Observation.") vCLog("UOM: Start Observation.")
// 使 // 使
// //
@ -378,9 +375,9 @@ public class InputHandler: InputHandlerProtocol {
prefs.failureFlagForUOMObservation = true prefs.failureFlagForUOMObservation = true
// //
// //
currentUOM.performObservation( currentLM.performUOMObservation(
walkedBefore: previousWalk, walkedAfter: currentWalk, cursor: actualNodeCursorPosition, walkedBefore: previousWalk, walkedAfter: currentWalk, cursor: actualNodeCursorPosition,
timestamp: Date().timeIntervalSince1970, saveCallback: { self.currentUOM.saveData() } timestamp: Date().timeIntervalSince1970
) )
// //
prefs.failureFlagForUOMObservation = false prefs.failureFlagForUOMObservation = false
@ -432,7 +429,7 @@ public class InputHandler: InputHandlerProtocol {
/// ///
if !prefs.fetchSuggestionsFromUserOverrideModel { return arrResult } if !prefs.fetchSuggestionsFromUserOverrideModel { return arrResult }
/// ///
let suggestion = currentUOM.fetchSuggestion( let suggestion = currentLM.fetchUOMSuggestion(
currentWalk: compositor.walkedNodes, cursor: actualNodeCursorPosition, timestamp: Date().timeIntervalSince1970 currentWalk: compositor.walkedNodes, cursor: actualNodeCursorPosition, timestamp: Date().timeIntervalSince1970
) )
arrResult.append(contentsOf: suggestion.candidates) arrResult.append(contentsOf: suggestion.candidates)

View File

@ -9,9 +9,9 @@
/// 調 /// 調
import CandidateWindow import CandidateWindow
import CocoaExtension
import InputMethodKit import InputMethodKit
import Megrez import Megrez
import OSFrameworkImpl
import Shared import Shared
// MARK: - § 調 (Handle Candidate State). // MARK: - § 調 (Handle Candidate State).
@ -30,6 +30,47 @@ extension InputHandler {
guard ctlCandidate.visible else { return false } guard ctlCandidate.visible else { return false }
let inputText = ignoringModifiers ? (input.inputTextIgnoringModifiers ?? input.text) : input.text let inputText = ignoringModifiers ? (input.inputTextIgnoringModifiers ?? input.text) : input.text
let allowMovingCompositorCursor = state.type == .ofCandidates && !prefs.useSCPCTypingMode let allowMovingCompositorCursor = state.type == .ofCandidates && !prefs.useSCPCTypingMode
let highlightedCandidate = state.candidates[ctlCandidate.highlightedIndex]
// MARK: Shift+?
var candidateTextServiceMenuRunning: Bool {
state.node.containsCandidateServices && state.type == .ofSymbolTable
}
serviceMenu: if prefs.useShiftQuestionToCallServiceMenu, input.commonKeyModifierFlags == .shift, input.text == "?" {
if candidateTextServiceMenuRunning { break serviceMenu }
let handled = handleServiceMenuInitiation(
candidateText: highlightedCandidate.value,
reading: highlightedCandidate.keyArray
)
if handled { return true }
}
// MARK: / /
if input.isSymbolMenuPhysicalKey {
switch input.commonKeyModifierFlags {
case .shift, [],
.option where !candidateTextServiceMenuRunning:
if !candidateTextServiceMenuRunning {
let handled = handleServiceMenuInitiation(
candidateText: highlightedCandidate.value,
reading: highlightedCandidate.keyArray
)
if handled { return true }
}
var updated = true
let reverseTrigger = input.isShiftHold || input.isOptionHold
updated = reverseTrigger ? ctlCandidate.showPreviousLine() : ctlCandidate.showNextLine()
if !updated { delegate.callError("66F3477B") }
return true
case .option where state.type == .ofSymbolTable:
//
return revolveTypingMethod(to: .haninKeyboardSymbol)
default: break
}
}
// MARK: 使 // MARK: 使
@ -101,7 +142,6 @@ extension InputHandler {
delegate.switchState(IMEState.ofAbortion()) delegate.switchState(IMEState.ofAbortion())
return true return true
} }
let highlightedCandidate = state.candidates[ctlCandidate.highlightedIndex] //
var handleAssociates = !prefs.useSCPCTypingMode && prefs.associatedPhrasesEnabled // var handleAssociates = !prefs.useSCPCTypingMode && prefs.associatedPhrasesEnabled //
handleAssociates = handleAssociates && compositor.cursor == compositor.length // handleAssociates = handleAssociates && compositor.cursor == compositor.length //
confirmHighlightedCandidate() confirmHighlightedCandidate()
@ -315,7 +355,7 @@ extension InputHandler {
} }
} }
// MARK: - Flipping pages by using modified bracket keys (when they are not occupied). // MARK: Flipping pages by using modified bracket keys (when they are not occupied).
// Shift+Command+[] Chrome Ctrl // Shift+Command+[] Chrome Ctrl
let ctrlCMD: Bool = input.commonKeyModifierFlags == [.control, .command] let ctrlCMD: Bool = input.commonKeyModifierFlags == [.control, .command]
@ -333,24 +373,6 @@ extension InputHandler {
} }
} }
// MARK: - Flipping pages by using symbol menu keys (when they are not occupied).
if input.isSymbolMenuPhysicalKey {
switch input.commonKeyModifierFlags {
case .shift, [],
.option where state.type != .ofSymbolTable:
var updated = true
let reverseTrigger = input.isShiftHold || input.isOptionHold
updated = reverseTrigger ? ctlCandidate.showPreviousLine() : ctlCandidate.showNextLine()
if !updated { delegate.callError("66F3477B") }
return true
case .option where state.type == .ofSymbolTable:
//
return revolveTypingMethod(to: .haninKeyboardSymbol)
default: break
}
}
if state.type == .ofInputting { return false } // `%quick` if state.type == .ofInputting { return false } // `%quick`
delegate.callError("172A0F81") delegate.callError("172A0F81")

View File

@ -58,7 +58,7 @@ private extension InputHandler {
func narrateTheComposer(with maybeKey: String? = nil, when condition: Bool, allowDuplicates: Bool = true) { func narrateTheComposer(with maybeKey: String? = nil, when condition: Bool, allowDuplicates: Bool = true) {
guard condition else { return } guard condition else { return }
let maybeKey = maybeKey ?? composer.phonabetKeyForQuery(pronouncable: prefs.acceptLeadingIntonations) let maybeKey = maybeKey ?? composer.phonabetKeyForQuery(pronounceableOnly: prefs.acceptLeadingIntonations)
guard var keyToNarrate = maybeKey else { return } guard var keyToNarrate = maybeKey else { return }
if composer.intonation == Tekkon.Phonabet(" ") { keyToNarrate.append("ˉ") } if composer.intonation == Tekkon.Phonabet(" ") { keyToNarrate.append("ˉ") }
SpeechSputnik.shared.narrate(keyToNarrate, allowDuplicates: allowDuplicates) SpeechSputnik.shared.narrate(keyToNarrate, allowDuplicates: allowDuplicates)
@ -119,7 +119,7 @@ private extension InputHandler {
return handleEnter(input: input, readingOnly: true) return handleEnter(input: input, readingOnly: true)
} }
// //
let maybeKey = composer.phonabetKeyForQuery(pronouncable: prefs.acceptLeadingIntonations) let maybeKey = composer.phonabetKeyForQuery(pronounceableOnly: prefs.acceptLeadingIntonations)
guard let readingKey = maybeKey else { break ifComposeReading } guard let readingKey = maybeKey else { break ifComposeReading }
// //
if !currentLM.hasUnigramsFor(keyArray: [readingKey]) { if !currentLM.hasUnigramsFor(keyArray: [readingKey]) {
@ -204,7 +204,7 @@ private extension InputHandler {
/// 調 /// 調
if keyConsumedByReading { if keyConsumedByReading {
// strict false // strict false
if composer.phonabetKeyForQuery(pronouncable: false) == nil { if composer.phonabetKeyForQuery(pronounceableOnly: false) == nil {
// 調 // 調
if !composer.isPinyinMode, input.isSpace, if !composer.isPinyinMode, input.isSpace,
compositor.insertKey(existedIntonation.value) compositor.insertKey(existedIntonation.value)
@ -422,16 +422,9 @@ private extension InputHandler {
delegate.switchState(updatedState) delegate.switchState(updatedState)
return true return true
} }
let encoding: CFStringEncodings? = {
switch IMEApp.currentInputMode {
case .imeModeCHS: return .GB_18030_2000
case .imeModeCHT: return .big5_HKSCS_1999
default: return nil
}
}()
guard guard
var char = "\(strCodePointBuffer)\(input.text)" var char = "\(strCodePointBuffer)\(input.text)"
.parsedAsHexLiteral(encoding: encoding)?.first?.description .parsedAsHexLiteral(encoding: IMEApp.currentInputMode.nonUTFEncoding)?.first?.description
else { else {
delegate.callError("D220B880輸入的字碼沒有對應的字元。") delegate.callError("D220B880輸入的字碼沒有對應的字元。")
var updatedState = IMEState.ofAbortion() var updatedState = IMEState.ofAbortion()

View File

@ -393,75 +393,6 @@ extension InputHandler {
return true return true
} }
// MARK: - Command+Enter
/// Command+Enter
/// - Parameter isShiftPressed: Shift
/// - Returns: SessionCtl IMK
private func commissionByCtrlCommandEnter(isShiftPressed: Bool = false) -> String {
var displayedText = compositor.keys.joined(separator: "\t")
if compositor.isEmpty {
displayedText = readingForDisplay
}
if !prefs.cassetteEnabled {
if prefs.inlineDumpPinyinInLieuOfZhuyin {
if !compositor.isEmpty {
var arrDisplayedTextElements = [String]()
compositor.keys.forEach { key in
arrDisplayedTextElements.append(Tekkon.restoreToneOneInPhona(target: key)) //
}
displayedText = arrDisplayedTextElements.joined(separator: "\t")
}
displayedText = Tekkon.cnvPhonaToHanyuPinyin(targetJoined: displayedText) //
}
if prefs.showHanyuPinyinInCompositionBuffer {
if compositor.isEmpty {
displayedText = displayedText.replacingOccurrences(of: "1", with: "")
}
}
}
displayedText = displayedText.replacingOccurrences(of: "\t", with: isShiftPressed ? "-" : " ")
return displayedText
}
// MARK: - Command+Option+Enter Ruby
/// Command+Option+Enter Ruby
/// - Parameter isShiftPressed: Shift
/// - Returns: SessionCtl IMK
private func commissionByCtrlOptionCommandEnter(isShiftPressed: Bool = false) -> String {
var composed = ""
compositor.walkedNodes.smashedPairs.forEach { key, value in
var key = key
if !prefs.cassetteEnabled {
key =
prefs.inlineDumpPinyinInLieuOfZhuyin
? Tekkon.restoreToneOneInPhona(target: key) //
: Tekkon.cnvPhonaToTextbookReading(target: key) //
if prefs.inlineDumpPinyinInLieuOfZhuyin {
key = Tekkon.cnvPhonaToHanyuPinyin(targetJoined: key) //
key = Tekkon.cnvHanyuPinyinToTextbookStyle(targetJoined: key) // 調
}
}
key = key.replacingOccurrences(of: "\t", with: " ")
if isShiftPressed {
if !composed.isEmpty { composed += " " }
composed += key.contains("_") ? "??" : key
return
}
//
composed += key.contains("_") ? value : "<ruby>\(value)<rp>(</rp><rt>\(key)</rt><rp>)</rp></ruby>"
}
return composed
}
// MARK: - BackSpace (macOS Delete) // MARK: - BackSpace (macOS Delete)
/// BackSpace (macOS Delete) /// BackSpace (macOS Delete)
@ -967,6 +898,20 @@ extension InputHandler {
} }
} }
// MARK: - (Service Menu)
func handleServiceMenuInitiation(candidateText: String, reading: [String]) -> Bool {
guard let delegate = delegate, delegate.state.type != .ofDeactivated else { return false }
guard !candidateText.isEmpty else { return false }
let rootNode = CandidateTextService.getCurrentServiceMenu(candidate: candidateText, reading: reading)
guard let rootNode = rootNode else { return false }
// commit buffer ESC
let textToCommit = generateStateOfInputting(sansReading: true).displayedText
delegate.switchState(IMEState.ofCommitting(textToCommit: textToCommit))
delegate.switchState(IMEState.ofSymbolTable(node: rootNode))
return true
}
// MARK: - Caps Lock Caps Lock and Alphanumerical mode // MARK: - Caps Lock Caps Lock and Alphanumerical mode
/// CapsLock /// CapsLock
@ -1096,11 +1041,15 @@ extension InputHandler {
let fullWidthResult = behaviorValue % 2 != 0 // let fullWidthResult = behaviorValue % 2 != 0 //
triagePrefs: switch (behaviorValue, isConsideredEmptyForNow) { triagePrefs: switch (behaviorValue, isConsideredEmptyForNow) {
case (2, _), (3, _), (4, false), (5, false): case (2, _), (3, _), (4, false), (5, false):
currentLM.config.numPadFWHWStatus = fullWidthResult currentLM.setOptions { config in
config.numPadFWHWStatus = fullWidthResult
}
if handlePunctuation("_NumPad_\(inputText)") { return true } if handlePunctuation("_NumPad_\(inputText)") { return true }
default: break triagePrefs // case 0 & 1 default: break triagePrefs // case 0 & 1
} }
currentLM.config.numPadFWHWStatus = nil currentLM.setOptions { config in
config.numPadFWHWStatus = nil
}
delegate.switchState(IMEState.ofEmpty()) delegate.switchState(IMEState.ofEmpty())
let charToCommit = inputText.applyingTransformFW2HW(reverse: fullWidthResult) let charToCommit = inputText.applyingTransformFW2HW(reverse: fullWidthResult)
delegate.switchState(IMEState.ofCommitting(textToCommit: charToCommit)) delegate.switchState(IMEState.ofCommitting(textToCommit: charToCommit))

View File

@ -0,0 +1,125 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import BrailleSputnik
import Shared
import Tekkon
///
extension InputHandler {
// MARK: - (Shift+)Ctrl+Command+Enter
/// Command+Enter
/// - Parameter isShiftPressed: Shift
/// - Returns: SessionCtl IMK
func commissionByCtrlCommandEnter(isShiftPressed: Bool = false) -> String {
var displayedText = compositor.keys.joined(separator: "\t")
if compositor.isEmpty {
displayedText = readingForDisplay
}
if !prefs.cassetteEnabled {
if prefs.inlineDumpPinyinInLieuOfZhuyin {
if !compositor.isEmpty {
var arrDisplayedTextElements = [String]()
compositor.keys.forEach { key in
arrDisplayedTextElements.append(Tekkon.restoreToneOneInPhona(target: key)) //
}
displayedText = arrDisplayedTextElements.joined(separator: "\t")
}
displayedText = Tekkon.cnvPhonaToHanyuPinyin(targetJoined: displayedText) //
}
if prefs.showHanyuPinyinInCompositionBuffer {
if compositor.isEmpty {
displayedText = displayedText.replacingOccurrences(of: "1", with: "")
}
}
}
displayedText = displayedText.replacingOccurrences(of: "\t", with: isShiftPressed ? "-" : " ")
return displayedText
}
// MARK: - (Shift+)Ctrl+Command+Option+Enter Ruby
private enum CommitableMarkupType: Int {
case bareKeys = -1
case textWithBracketedAnnotations = 0
case textWithHTMLRubyAnnotations = 1
case braille1947 = 2
case braille2018 = 3
static func match(rawValue: Int) -> Self {
CommitableMarkupType(rawValue: rawValue) ?? .textWithBracketedAnnotations
}
var brailleStandard: BrailleSputnik.BrailleStandard? {
switch self {
case .braille1947: return .of1947
case .braille2018: return .of2018
default: return nil
}
}
}
/// Command+Option+Enter Ruby
///
/// prefs.specifyCmdOptCtrlEnterBehavior
/// 1.
/// 2. HTML Ruby
/// 3. (1947)
/// 4. (GF0019-2018)
/// - Parameter isShiftPressed: Shift
/// - Returns: SessionCtl IMK
func commissionByCtrlOptionCommandEnter(isShiftPressed: Bool = false) -> String {
var behavior = CommitableMarkupType.match(rawValue: prefs.specifyCmdOptCtrlEnterBehavior)
if prefs.cassetteEnabled, behavior.brailleStandard != nil {
behavior = .textWithBracketedAnnotations
}
if isShiftPressed { behavior = .bareKeys }
guard let brailleStandard = behavior.brailleStandard else {
return specifyTextMarkupToCommit(behavior: behavior)
}
let brailleProcessor = BrailleSputnik(standard: brailleStandard)
return brailleProcessor.convertToBraille(
smashedPairs: compositor.walkedNodes.smashedPairs,
extraInsertion: (reading: composer.value, cursor: compositor.cursor)
)
}
private func specifyTextMarkupToCommit(behavior: CommitableMarkupType) -> String {
var composed = ""
compositor.walkedNodes.smashedPairs.forEach { key, value in
var key = key
if !prefs.cassetteEnabled {
key =
prefs.inlineDumpPinyinInLieuOfZhuyin
? Tekkon.restoreToneOneInPhona(target: key) //
: Tekkon.cnvPhonaToTextbookStyle(target: key) //
if prefs.inlineDumpPinyinInLieuOfZhuyin {
key = Tekkon.cnvPhonaToHanyuPinyin(targetJoined: key) //
key = Tekkon.cnvHanyuPinyinToTextbookStyle(targetJoined: key) // 調
}
}
key = key.replacingOccurrences(of: "\t", with: " ")
switch behavior {
case .bareKeys:
if !composed.isEmpty { composed += " " }
composed += key.contains("_") ? "??" : key
case .textWithBracketedAnnotations:
composed += key.contains("_") ? value : "\(value)(\(key))"
case .textWithHTMLRubyAnnotations:
composed += key.contains("_") ? value : "<ruby>\(value)<rp>(</rp><rt>\(key)</rt><rp>)</rp></ruby>"
case .braille1947: break //
case .braille2018: break //
}
}
return composed
}
}

View File

@ -9,11 +9,11 @@
/// 調 IMK 調 /// 調 IMK 調
/// 調 /// 調
import CocoaExtension
import IMKUtils import IMKUtils
import InputMethodKit import InputMethodKit
import LangModelAssembly import LangModelAssembly
import Megrez import Megrez
import OSFrameworkImpl
import Shared import Shared
// MARK: - § 調 (Handle Input with States) * Triage // MARK: - § 調 (Handle Input with States) * Triage

View File

@ -7,6 +7,7 @@
// requirements defined in MIT License. // requirements defined in MIT License.
import Foundation import Foundation
import Shared
import SwiftExtension import SwiftExtension
// MARK: - Typing Method // MARK: - Typing Method
@ -30,12 +31,8 @@ public extension InputHandler {
case .codePoint: case .codePoint:
let commonTerm = NSMutableString() let commonTerm = NSMutableString()
commonTerm.insert("Code Point Input.".localized, at: 0) commonTerm.insert("Code Point Input.".localized, at: 0)
if !vertical { if !vertical, let initials = IMEApp.currentInputMode.nonUTFEncodingInitials {
switch IMEApp.currentInputMode { commonTerm.insert("[\(initials)] ", at: 0)
case .imeModeCHS: commonTerm.insert("[GB] ", at: 0)
case .imeModeCHT: commonTerm.insert("[Big5] ", at: 0)
default: break
}
} }
return commonTerm.description return commonTerm.description
case .haninKeyboardSymbol: case .haninKeyboardSymbol:

View File

@ -15,26 +15,20 @@ import SwiftExtension
// MARK: - Input Mode Extension for Language Models // MARK: - Input Mode Extension for Language Models
public extension Shared.InputMode { public extension Shared.InputMode {
private static let lmCHS = vChewingLM.LMInstantiator(isCHS: true) private static let lmCHS = LMAssembly.LMInstantiator(
private static let lmCHT = vChewingLM.LMInstantiator(isCHS: false) isCHS: true, uomDataURL: LMMgr.userOverrideModelDataURL(.imeModeCHS)
private static let uomCHS = vChewingLM.LMUserOverride(dataURL: LMMgr.userOverrideModelDataURL(.imeModeCHS)) )
private static let uomCHT = vChewingLM.LMUserOverride(dataURL: LMMgr.userOverrideModelDataURL(.imeModeCHT)) private static let lmCHT = LMAssembly.LMInstantiator(
isCHS: false, uomDataURL: LMMgr.userOverrideModelDataURL(.imeModeCHT)
)
var langModel: vChewingLM.LMInstantiator { var langModel: LMAssembly.LMInstantiator {
switch self { switch self {
case .imeModeCHS: return Self.lmCHS case .imeModeCHS: return Self.lmCHS
case .imeModeCHT: return Self.lmCHT case .imeModeCHT: return Self.lmCHT
case .imeModeNULL: return .init() case .imeModeNULL: return .init()
} }
} }
var uom: vChewingLM.LMUserOverride {
switch self {
case .imeModeCHS: return Self.uomCHS
case .imeModeCHT: return Self.uomCHT
case .imeModeNULL: return .init(dataURL: LMMgr.userOverrideModelDataURL(IMEApp.currentInputMode))
}
}
} }
// MARK: - Language Model Manager. // MARK: - Language Model Manager.
@ -54,14 +48,14 @@ public class LMMgr {
Self.loadUserPhrasesData() Self.loadUserPhrasesData()
} }
public static var isCoreDBConnected: Bool { vChewingLM.LMInstantiator.isSQLDBConnected } public static var isCoreDBConnected: Bool { LMAssembly.LMInstantiator.isSQLDBConnected }
public static func connectCoreDB(dbPath: String? = nil) { public static func connectCoreDB(dbPath: String? = nil) {
guard let path: String = dbPath ?? Self.getCoreDictionaryDBPath() else { guard let path: String = dbPath ?? Self.getCoreDictionaryDBPath() else {
assertionFailure("vChewing factory SQLite data not found.") assertionFailure("vChewing factory SQLite data not found.")
return return
} }
let result = vChewingLM.LMInstantiator.connectSQLDB(dbPath: path) let result = LMAssembly.LMInstantiator.connectSQLDB(dbPath: path)
assert(result, "vChewing factory SQLite connection failed.") assert(result, "vChewing factory SQLite connection failed.")
Notifier.notify( Notifier.notify(
message: NSLocalizedString("Core Dict loading complete.", comment: "") message: NSLocalizedString("Core Dict loading complete.", comment: "")
@ -71,10 +65,15 @@ public class LMMgr {
/// ///
/// - Remark: cassettePath() /// - Remark: cassettePath()
public static func loadCassetteData() { public static func loadCassetteData() {
vChewingLM.LMInstantiator.loadCassetteData(path: cassettePath()) func validateCassetteCandidateKey(_ target: String) -> Bool {
CandidateKey.validate(keys: target) == nil
}
LMAssembly.LMInstantiator.setCassetCandidateKeyValidator(validateCassetteCandidateKey)
LMAssembly.LMInstantiator.loadCassetteData(path: cassettePath())
} }
public static func loadUserPhrasesData(type: vChewingLM.ReplacableUserDataType? = nil) { public static func loadUserPhrasesData(type: LMAssembly.ReplacableUserDataType? = nil) {
guard let type = type else { guard let type = type else {
Shared.InputMode.validCases.forEach { mode in Shared.InputMode.validCases.forEach { mode in
mode.langModel.loadUserPhrasesData( mode.langModel.loadUserPhrasesData(
@ -82,12 +81,11 @@ public class LMMgr {
filterPath: userDictDataURL(mode: mode, type: .theFilter).path filterPath: userDictDataURL(mode: mode, type: .theFilter).path
) )
mode.langModel.loadUserSymbolData(path: userDictDataURL(mode: mode, type: .theSymbols).path) mode.langModel.loadUserSymbolData(path: userDictDataURL(mode: mode, type: .theSymbols).path)
mode.uom.loadData(fromURL: userOverrideModelDataURL(mode)) mode.langModel.loadUOMData()
} }
if PrefMgr.shared.associatedPhrasesEnabled { Self.loadUserAssociatesData() } if PrefMgr.shared.associatedPhrasesEnabled { Self.loadUserAssociatesData() }
if PrefMgr.shared.phraseReplacementEnabled { Self.loadUserPhraseReplacement() } if PrefMgr.shared.phraseReplacementEnabled { Self.loadUserPhraseReplacement() }
if PrefMgr.shared.useSCPCTypingMode { Self.loadSCPCSequencesData() }
CandidateNode.load(url: Self.userSymbolMenuDataURL()) CandidateNode.load(url: Self.userSymbolMenuDataURL())
return return
@ -131,12 +129,6 @@ public class LMMgr {
} }
} }
public static func loadSCPCSequencesData() {
Shared.InputMode.validCases.forEach { mode in
mode.langModel.loadSCPCSequencesData()
}
}
public static func reloadUserFilterDirectly(mode: Shared.InputMode) { public static func reloadUserFilterDirectly(mode: Shared.InputMode) {
mode.langModel.reloadUserFilterDirectly(path: userDictDataURL(mode: mode, type: .theFilter).path) mode.langModel.reloadUserFilterDirectly(path: userDictDataURL(mode: mode, type: .theFilter).path)
} }
@ -187,12 +179,12 @@ public class LMMgr {
// MARK: UOM // MARK: UOM
public static func saveUserOverrideModelData() { public static func saveUserOverrideModelData() {
let globalQueue = DispatchQueue(label: "vChewingLM_UOM", qos: .unspecified, attributes: .concurrent) let globalQueue = DispatchQueue(label: "LMAssembly_UOM", qos: .unspecified, attributes: .concurrent)
let group = DispatchGroup() let group = DispatchGroup()
Shared.InputMode.validCases.forEach { mode in Shared.InputMode.validCases.forEach { mode in
group.enter() group.enter()
globalQueue.async { globalQueue.async {
mode.uom.saveData(toURL: userOverrideModelDataURL(mode)) mode.langModel.saveUOMData()
group.leave() group.leave()
} }
} }
@ -201,11 +193,11 @@ public class LMMgr {
} }
public static func bleachSpecifiedSuggestions(targets: [String], mode: Shared.InputMode) { public static func bleachSpecifiedSuggestions(targets: [String], mode: Shared.InputMode) {
mode.uom.bleachSpecifiedSuggestions(targets: targets, saveCallback: { mode.uom.saveData() }) mode.langModel.bleachSpecifiedUOMSuggestions(targets: targets)
} }
public static func removeUnigramsFromUserOverrideModel(_ mode: Shared.InputMode) { public static func removeUnigramsFromUserOverrideModel(_ mode: Shared.InputMode) {
mode.uom.bleachUnigrams(saveCallback: { mode.uom.saveData() }) mode.langModel.bleachUOMUnigrams()
} }
public static func relocateWreckedUOMData() { public static func relocateWreckedUOMData() {
@ -227,6 +219,6 @@ public class LMMgr {
} }
public static func clearUserOverrideModelData(_ mode: Shared.InputMode = .imeModeNULL) { public static func clearUserOverrideModelData(_ mode: Shared.InputMode = .imeModeNULL) {
mode.uom.clearData(withURL: userOverrideModelDataURL(mode)) mode.langModel.clearUOMData()
} }
} }

View File

@ -17,23 +17,23 @@ import Shared
extension LMMgr: PhraseEditorDelegate { extension LMMgr: PhraseEditorDelegate {
public var currentInputMode: Shared.InputMode { IMEApp.currentInputMode } public var currentInputMode: Shared.InputMode { IMEApp.currentInputMode }
public func openPhraseFile(mode: Shared.InputMode, type: vChewingLM.ReplacableUserDataType, using app: FileOpenMethod) { public func openPhraseFile(mode: Shared.InputMode, type: LMAssembly.ReplacableUserDataType, using app: FileOpenMethod) {
Self.openPhraseFile(fromURL: Self.userDictDataURL(mode: mode, type: type), using: app) Self.openPhraseFile(fromURL: Self.userDictDataURL(mode: mode, type: type), using: app)
} }
public func consolidate(text strProcessed: inout String, pragma shouldCheckPragma: Bool) { public func consolidate(text strProcessed: inout String, pragma shouldCheckPragma: Bool) {
vChewingLM.LMConsolidator.consolidate(text: &strProcessed, pragma: shouldCheckPragma) LMAssembly.LMConsolidator.consolidate(text: &strProcessed, pragma: shouldCheckPragma)
} }
public func checkIfPhrasePairExists(userPhrase: String, mode: Shared.InputMode, key unigramKey: String) -> Bool { public func checkIfPhrasePairExists(userPhrase: String, mode: Shared.InputMode, key unigramKey: String) -> Bool {
Self.checkIfPhrasePairExists(userPhrase: userPhrase, mode: mode, keyArray: [unigramKey]) Self.checkIfPhrasePairExists(userPhrase: userPhrase, mode: mode, keyArray: [unigramKey])
} }
public func retrieveData(mode: Shared.InputMode, type: vChewingLM.ReplacableUserDataType) -> String { public func retrieveData(mode: Shared.InputMode, type: LMAssembly.ReplacableUserDataType) -> String {
Self.retrieveData(mode: mode, type: type) Self.retrieveData(mode: mode, type: type)
} }
public static func retrieveData(mode: Shared.InputMode, type: vChewingLM.ReplacableUserDataType) -> String { public static func retrieveData(mode: Shared.InputMode, type: LMAssembly.ReplacableUserDataType) -> String {
vCLog("Retrieving data. Mode: \(mode.localizedDescription), type: \(type.localizedDescription)") vCLog("Retrieving data. Mode: \(mode.localizedDescription), type: \(type.localizedDescription)")
let theURL = Self.userDictDataURL(mode: mode, type: type) let theURL = Self.userDictDataURL(mode: mode, type: type)
do { do {
@ -44,12 +44,12 @@ extension LMMgr: PhraseEditorDelegate {
} }
} }
public func saveData(mode: Shared.InputMode, type: vChewingLM.ReplacableUserDataType, data: String) -> String { public func saveData(mode: Shared.InputMode, type: LMAssembly.ReplacableUserDataType, data: String) -> String {
Self.saveData(mode: mode, type: type, data: data) Self.saveData(mode: mode, type: type, data: data)
} }
@discardableResult public static func saveData( @discardableResult public static func saveData(
mode: Shared.InputMode, type: vChewingLM.ReplacableUserDataType, data: String mode: Shared.InputMode, type: LMAssembly.ReplacableUserDataType, data: String
) -> String { ) -> String {
DispatchQueue.main.async { DispatchQueue.main.async {
let theURL = Self.userDictDataURL(mode: mode, type: type) let theURL = Self.userDictDataURL(mode: mode, type: type)

View File

@ -6,8 +6,10 @@
// marks, or product names of Contributor, except as required to fulfill notice // marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License. // requirements defined in MIT License.
import CandidateWindow
import Foundation import Foundation
import LangModelAssembly import LangModelAssembly
import Megrez
import Shared import Shared
// MARK: - 使 // MARK: - 使
@ -32,6 +34,10 @@ public extension LMMgr {
!keyArray.isEmpty && keyArray.filter(\.isEmpty).isEmpty && !value.isEmpty !keyArray.isEmpty && keyArray.filter(\.isEmpty).isEmpty && !value.isEmpty
} }
var isSingleCharReadingPair: Bool {
value.count == 1 && keyArray.count == 1 && keyArray.first?.first != "_"
}
public var description: String { public var description: String {
descriptionCells.joined(separator: " ") descriptionCells.joined(separator: " ")
} }
@ -88,7 +94,7 @@ public extension LMMgr {
/// 使 /// 使
/// ///
/// ///
let theType: vChewingLM.ReplacableUserDataType = toFilter ? .theFilter : .thePhrases let theType: LMAssembly.ReplacableUserDataType = toFilter ? .theFilter : .thePhrases
let theURL = LMMgr.userDictDataURL(mode: inputMode, type: theType) let theURL = LMMgr.userDictDataURL(mode: inputMode, type: theType)
var fileSize: UInt64? var fileSize: UInt64?
do { do {
@ -143,7 +149,7 @@ public extension LMMgr {
} }
} }
let theURL = LMMgr.userDictDataURL(mode: inputMode, type: .theFilter) let theURL = LMMgr.userDictDataURL(mode: inputMode, type: .theFilter)
if forceConsolidate, !vChewingLM.LMConsolidator.consolidate(path: theURL.path, pragma: false) { return false } if forceConsolidate, !LMAssembly.LMConsolidator.consolidate(path: theURL.path, pragma: false) { return false }
// Get FileSize. // Get FileSize.
var fileSize: UInt64? var fileSize: UInt64?
do { do {
@ -190,3 +196,51 @@ public extension LMMgr {
} }
} }
} }
// MARK: - Weight Suggestions.
public extension LMMgr.UserPhrase {
mutating func updateWeight(basedOn action: CandidateContextMenuAction) {
weight = suggestNextFreq(for: action)
}
func suggestNextFreq(for action: CandidateContextMenuAction, extreme: Bool = false) -> Double? {
var extremeFallbackResult: Double? {
switch action {
case .toBoost: return nil // 0
case .toNerf: return -114.514
case .toFilter: return nil
}
}
guard !extreme, isSingleCharReadingPair else { return extremeFallbackResult }
let fetchedUnigrams = inputMode.langModel.unigramsFor(keyArray: keyArray)
let currentWeight = weight ?? fetchedUnigrams.first { $0.value == value }?.score
guard let currentWeight = currentWeight else { return extremeFallbackResult }
let fetchedScores = fetchedUnigrams.map(\.score)
var neighborValue: Double?
switch action {
case .toBoost:
neighborValue = currentWeight.findNeighborValue(from: fetchedScores, greater: true)
if let realNeighborValue = neighborValue {
neighborValue = realNeighborValue + 0.000_001
} else if let fetchedMax = fetchedScores.min(), currentWeight <= fetchedMax {
neighborValue = Swift.min(0, currentWeight + 0.000_001)
} else {
//
neighborValue = Swift.min(0, currentWeight + 1)
}
case .toNerf:
neighborValue = currentWeight.findNeighborValue(from: fetchedScores, greater: false)
if let realNeighborValue = neighborValue {
neighborValue = realNeighborValue - 0.000_001
} else if let fetchedMax = fetchedScores.max(), currentWeight >= fetchedMax {
neighborValue = Swift.max(-114.514, currentWeight - 0.000_001)
} else {
//
neighborValue = Swift.max(-114.514, currentWeight - 1)
}
case .toFilter: return nil
}
return neighborValue ?? extremeFallbackResult
}
}

View File

@ -61,7 +61,7 @@ public extension LMMgr {
/// - mode: /// - mode:
/// - type: /// - type:
/// - Returns: URL /// - Returns: URL
static func userDictDataURL(mode: Shared.InputMode, type: vChewingLM.ReplacableUserDataType) -> URL { static func userDictDataURL(mode: Shared.InputMode, type: LMAssembly.ReplacableUserDataType) -> URL {
var fileName: String = { var fileName: String = {
switch type { switch type {
case .thePhrases: return "userdata" case .thePhrases: return "userdata"
@ -271,7 +271,7 @@ public extension LMMgr {
return true return true
} }
static func openUserDictFile(type: vChewingLM.ReplacableUserDataType, dual: Bool = false, alt: Bool) { static func openUserDictFile(type: LMAssembly.ReplacableUserDataType, dual: Bool = false, alt: Bool) {
let app: FileOpenMethod = alt ? .textEdit : .finder let app: FileOpenMethod = alt ? .textEdit : .finder
openPhraseFile(fromURL: userDictDataURL(mode: IMEApp.currentInputMode, type: type), using: app) openPhraseFile(fromURL: userDictDataURL(mode: IMEApp.currentInputMode, type: type), using: app)
guard dual else { return } guard dual else { return }
@ -324,7 +324,7 @@ public extension LMMgr {
/// ///
/// ///
var failed = false var failed = false
caseCheck: for type in vChewingLM.ReplacableUserDataType.allCases { caseCheck: for type in LMAssembly.ReplacableUserDataType.allCases {
let templateName = Self.templateName(for: type, mode: mode) let templateName = Self.templateName(for: type, mode: mode)
if !ensureFileExists(userDictDataURL(mode: mode, type: type), deployTemplate: templateName) { if !ensureFileExists(userDictDataURL(mode: mode, type: type), deployTemplate: templateName) {
failed = true failed = true
@ -334,7 +334,7 @@ public extension LMMgr {
return !failed return !failed
} }
internal static func templateName(for type: vChewingLM.ReplacableUserDataType, mode: Shared.InputMode) -> String { internal static func templateName(for type: LMAssembly.ReplacableUserDataType, mode: Shared.InputMode) -> String {
switch type { switch type {
case .thePhrases: return kTemplateNameUserPhrases case .thePhrases: return kTemplateNameUserPhrases
case .theFilter: return kTemplateNameUserFilterList case .theFilter: return kTemplateNameUserFilterList
@ -345,3 +345,26 @@ public extension LMMgr {
} }
} }
} }
// MARK: - 使 JSON
// JSON 便
public extension LMMgr {
@discardableResult static func dumpUserDictDataToJSON(print: Bool = false, all: Bool) -> String? {
var summarizedDict = [LMAssembly.UserDictionarySummarized]()
Shared.InputMode.allCases.forEach { mode in
guard mode != .imeModeNULL else { return }
summarizedDict.append(mode.langModel.summarize(all: all))
}
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
if #available(macOS 10.13, *) {
encoder.outputFormatting.insert(.sortedKeys)
}
guard let data = try? encoder.encode(summarizedDict) else { return nil }
guard let outputStr = String(data: data, encoding: .utf8) else { return nil }
if print { Swift.print(outputStr) }
return outputStr
}
}

View File

@ -0,0 +1,36 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import Shared
public extension PrefMgr {
static let shared: PrefMgr = {
let result = PrefMgr()
result.assignDidSetActions()
return result
}()
private func assignDidSetActions() {
didAskForSyncingLMPrefs = {
if PrefMgr.shared.phraseReplacementEnabled {
LMMgr.loadUserPhraseReplacement()
}
if PrefMgr.shared.associatedPhrasesEnabled {
LMMgr.loadUserAssociatesData()
}
LMMgr.syncLMPrefs()
}
didAskForRefreshingSpeechSputnik = {
SpeechSputnik.shared.refreshStatus()
}
didAskForSyncingShiftKeyDetectorPrefs = {
SessionCtl.theShiftKeyDetector.toggleWithLShift = PrefMgr.shared.togglingAlphanumericalModeWithLShift
SessionCtl.theShiftKeyDetector.toggleWithRShift = PrefMgr.shared.togglingAlphanumericalModeWithRShift
}
}
}

View File

@ -150,7 +150,7 @@ class FrmRevLookupWindow: NSWindow {
strBuilder.append("Maximum 15 results returnable.".localized + "\n") strBuilder.append("Maximum 15 results returnable.".localized + "\n")
break theLoop break theLoop
} }
let arrResult = vChewingLM.LMInstantiator.getFactoryReverseLookupData(with: char)?.deduplicated ?? [] let arrResult = LMAssembly.LMInstantiator.getFactoryReverseLookupData(with: char)?.deduplicated ?? []
if !arrResult.isEmpty { if !arrResult.isEmpty {
strBuilder.append(char + "\t") strBuilder.append(char + "\t")
strBuilder.append(arrResult.joined(separator: ", ")) strBuilder.append(arrResult.joined(separator: ", "))

View File

@ -8,7 +8,7 @@
import AppKit import AppKit
import Carbon import Carbon
import CocoaExtension import OSFrameworkImpl
import Shared import Shared
public class SecurityAgentHelper { public class SecurityAgentHelper {

View File

@ -0,0 +1,63 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import AppKit
import Foundation
public class CtlServiceMenuEditor: NSWindowController {
let viewController = VwrServiceMenuEditor()
public static var shared: CtlServiceMenuEditor?
public init() {
super.init(
window: .init(
contentRect: CGRect(x: 401, y: 295, width: 770, height: 335),
styleMask: [.titled, .closable, .miniaturizable],
backing: .buffered,
defer: true
)
)
viewController.windowController = self
viewController.loadView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
public static func show() {
if shared == nil {
shared = CtlServiceMenuEditor()
}
guard let shared = shared, let sharedWindow = shared.window else { return }
if !sharedWindow.isVisible {
shared.windowDidLoad()
}
sharedWindow.setPosition(vertical: .center, horizontal: .right, padding: 20)
sharedWindow.orderFrontRegardless() //
sharedWindow.title = "Service Menu Editor".localized
sharedWindow.level = .statusBar
if #available(macOS 10.10, *) {
sharedWindow.titlebarAppearsTransparent = true
}
shared.showWindow(shared)
NSApp.popup()
}
override public func windowDidLoad() {
super.windowDidLoad()
let view = viewController.view
window?.contentView = view
if let window = window {
var frame = window.frame
frame.size = view.fittingSize
window.setFrame(frame, display: true)
}
window?.setPosition(vertical: .center, horizontal: .right, padding: 20)
}
}

View File

@ -0,0 +1,368 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import AppKit
import Foundation
import OSFrameworkImpl
import Shared
public class VwrServiceMenuEditor: NSViewController {
let windowWidth: CGFloat = 770
let contentWidth: CGFloat = 750
let tableHeight: CGFloat = 230
lazy var tblServices: NSTableView = .init()
lazy var btnShowInstructions = NSButton("How to Fill", target: self, action: #selector(btnShowInstructionsClicked(_:)))
lazy var btnAddService = NSFileDragRetrieverButton(
"Add Service",
target: self,
action: #selector(btnAddServiceClicked(_:)),
postDrag: handleDrag
)
lazy var btnRemoveService = NSButton("Remove Selected", target: self, action: #selector(btnRemoveServiceClicked(_:)))
lazy var btnResetService = NSButton("Reset Default", target: self, action: #selector(btnResetServiceClicked(_:)))
lazy var btnCopyAllToClipboard = NSButton("Copy All to Clipboard", target: self, action: #selector(btnCopyAllToClipboardClicked(_:)))
lazy var tableColumn1Cell = NSTextFieldCell()
lazy var tableColumn1 = NSTableColumn()
lazy var tableColumn2Cell = NSTextFieldCell()
lazy var tableColumn2 = NSTableColumn()
var windowController: NSWindowController?
public convenience init(windowController: NSWindowController? = nil) {
self.init()
self.windowController = windowController
}
override public func loadView() {
tblServices.reloadData()
view = body ?? .init()
(view as? NSStackView)?.alignment = .centerX
view.makeSimpleConstraint(.width, relation: .equal, value: windowWidth)
btnRemoveService.keyEquivalent = .init(NSEvent.SpecialKey.delete.unicodeScalar)
}
var body: NSView? {
NSStackView.build(.vertical, insets: .new(all: 14)) {
NSStackView.build(.horizontal) {
btnAddService
btnRemoveService
btnCopyAllToClipboard
btnShowInstructions
NSView()
btnResetService
}
makeScrollableTable()
.makeSimpleConstraint(.height, relation: .equal, value: tableHeight)
NSStackView.build(.horizontal) {
let descriptionWidth = contentWidth - 10
NSStackView.build(.vertical) {
let strDescription = "i18n:CandidateServiceMenuEditor.description"
strDescription.makeNSLabel(descriptive: true, fixWidth: descriptionWidth)
.makeSimpleConstraint(.width, relation: .greaterThanOrEqual, value: descriptionWidth)
NSView()
}
}
}
}
func makeScrollableTable() -> NSScrollView {
let scrollContainer = NSScrollView()
scrollContainer.scrollerStyle = .legacy
scrollContainer.autohidesScrollers = true
scrollContainer.documentView = tblServices
scrollContainer.hasVerticalScroller = true
scrollContainer.hasHorizontalScroller = true
if #available(macOS 11.0, *) {
tblServices.style = .inset
}
tblServices.addTableColumn(tableColumn1)
tblServices.addTableColumn(tableColumn2)
// tblServices.headerView = nil
tblServices.delegate = self
tblServices.allowsExpansionToolTips = true
tblServices.allowsMultipleSelection = true
tblServices.autoresizingMask = [.width, .height]
tblServices.autosaveTableColumns = false
tblServices.backgroundColor = NSColor.controlBackgroundColor
tblServices.columnAutoresizingStyle = .lastColumnOnlyAutoresizingStyle
tblServices.frame = CGRect(x: 0, y: 0, width: 728, height: tableHeight)
tblServices.gridColor = NSColor.clear
tblServices.intercellSpacing = CGSize(width: 15, height: 0)
tblServices.setContentHuggingPriority(.defaultHigh, for: .vertical)
tblServices.registerForDraggedTypes([.kUTTypeData, .kUTTypeFileURL])
tblServices.dataSource = self
tblServices.target = self
if #available(macOS 11.0, *) { tblServices.style = .inset }
tableColumn1.identifier = NSUserInterfaceItemIdentifier("colTitle")
tableColumn1.headerCell.title = "i18n:CandidateServiceMenuEditor.table.field.MenuTitle".localized
tableColumn1.maxWidth = 280
tableColumn1.minWidth = 200
tableColumn1.resizingMask = [.autoresizingMask, .userResizingMask]
tableColumn1.width = 200
tableColumn1.dataCell = tableColumn1Cell
tableColumn1Cell.font = NSFont.systemFont(ofSize: 13)
tableColumn1Cell.isEditable = true
tableColumn1Cell.isSelectable = true
tableColumn1Cell.lineBreakMode = .byTruncatingTail
tableColumn1Cell.stringValue = "Text Cell"
tableColumn1Cell.textColor = NSColor.controlTextColor
tableColumn2.identifier = NSUserInterfaceItemIdentifier("colValue")
tableColumn2.headerCell.title = "i18n:CandidateServiceMenuEditor.table.field.Value".localized
tableColumn2.maxWidth = 1000
tableColumn2.minWidth = 40
tableColumn2.resizingMask = [.autoresizingMask, .userResizingMask]
tableColumn2.width = 480
tableColumn2.dataCell = tableColumn2Cell
tableColumn2Cell.backgroundColor = NSColor.controlBackgroundColor
tableColumn2Cell.font = NSFont.systemFont(ofSize: 13)
tableColumn2Cell.isEditable = true
tableColumn2Cell.isSelectable = true
tableColumn2Cell.lineBreakMode = .byTruncatingTail
tableColumn2Cell.stringValue = "Text Cell"
tableColumn2Cell.textColor = NSColor.controlTextColor
return scrollContainer
}
}
// MARK: - UserDefaults Handlers.
public extension VwrServiceMenuEditor {
static var servicesList: [CandidateTextService] {
get {
PrefMgr.shared.candidateServiceMenuContents.parseIntoCandidateTextServiceStack()
}
set {
PrefMgr.shared.candidateServiceMenuContents = newValue.rawRepresentation
}
}
static func removeService(at index: Int) {
guard index < Self.servicesList.count else { return }
Self.servicesList.remove(at: index)
}
}
// MARK: - Common Operation Methods.
extension VwrServiceMenuEditor {
func refresh() {
tblServices.reloadData()
reassureButtonAvailability()
}
func reassureButtonAvailability() {
btnRemoveService.isEnabled = (0 ..< Self.servicesList.count).contains(
tblServices.selectedRow)
}
func handleDrag(_ givenURL: URL) {
guard let string = try? String(contentsOf: givenURL) else { return }
Self.servicesList.append(contentsOf: string.components(separatedBy: .newlines).parseIntoCandidateTextServiceStack())
refresh()
}
}
// MARK: - IBActions.
extension VwrServiceMenuEditor {
@IBAction func btnShowInstructionsClicked(_: Any) {
let strTitle = "How to Fill".localized
let strFillGuide = "i18n:CandidateServiceMenuEditor.formatGuide".localized
windowController?.window.callAlert(title: strTitle, text: strFillGuide)
}
@IBAction func btnResetServiceClicked(_: Any) {
PrefMgr.shared.candidateServiceMenuContents = PrefMgr.kDefaultCandidateServiceMenuItem
tblServices.reloadData()
}
@IBAction func btnCopyAllToClipboardClicked(_: Any) {
var resultArrayLines = [String]()
Self.servicesList.forEach { currentService in
resultArrayLines.append("\(currentService.key)\t\(currentService.definedValue)")
}
let result = resultArrayLines.joined(separator: "\n").appending("\n")
NSPasteboard.general.declareTypes([.string], owner: nil)
NSPasteboard.general.setString(result, forType: .string)
}
@IBAction func btnRemoveServiceClicked(_: Any) {
guard let minIndexSelected = tblServices.selectedRowIndexes.min() else { return }
if minIndexSelected >= Self.servicesList.count { return }
if minIndexSelected < 0 { return }
var isLastRow = false
tblServices.selectedRowIndexes.sorted().reversed().forEach { index in
isLastRow = {
if Self.servicesList.count < 2 { return false }
return minIndexSelected == Self.servicesList.count - 1
}()
if index < Self.servicesList.count {
Self.removeService(at: index)
}
}
if isLastRow {
tblServices.selectRowIndexes(.init(arrayLiteral: minIndexSelected - 1), byExtendingSelection: false)
}
tblServices.reloadData()
btnRemoveService.isEnabled = (0 ..< Self.servicesList.count).contains(minIndexSelected)
}
@IBAction func btnAddServiceClicked(_: Any) {
guard let window = windowController?.window else { return }
let alert = NSAlert()
alert.messageText = NSLocalizedString(
"i18n:CandidateServiceMenuEditor.prompt", comment: ""
)
alert.informativeText = NSLocalizedString(
"i18n:CandidateServiceMenuEditor.howToGetGuide", comment: ""
)
alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
let maxFloat = Double(Float.greatestFiniteMagnitude)
let scrollview = NSScrollView(frame: NSRect(x: 0, y: 0, width: 512, height: 200))
let contentSize = scrollview.contentSize
scrollview.borderType = .noBorder
scrollview.hasVerticalScroller = true
scrollview.hasHorizontalScroller = true
scrollview.horizontalScroller?.scrollerStyle = .legacy
scrollview.verticalScroller?.scrollerStyle = .legacy
scrollview.autoresizingMask = [.width, .height]
let theTextView = NSTextView(frame: NSRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height))
scrollview.documentView = theTextView
theTextView.minSize = NSSize(width: 0.0, height: contentSize.height)
theTextView.maxSize = NSSize(width: maxFloat, height: maxFloat)
theTextView.isVerticallyResizable = true
theTextView.isHorizontallyResizable = false
theTextView.autoresizingMask = .width
theTextView.textContainer?.containerSize = NSSize(width: contentSize.width, height: maxFloat)
theTextView.textContainer?.widthTracksTextView = true
theTextView.enclosingScrollView?.hasHorizontalScroller = true
theTextView.isHorizontallyResizable = true
theTextView.autoresizingMask = [.width, .height]
theTextView.textContainer?.containerSize = NSSize(width: maxFloat, height: maxFloat)
theTextView.textContainer?.widthTracksTextView = false
theTextView.toolTip = "i18n:CandidateServiceMenuEditor.formatGuide".localized
alert.accessoryView = scrollview
alert.beginSheetModal(at: window) { result in
switch result {
case .alertFirstButtonReturn:
let rawLines = theTextView.textContainer?.textView?.string.components(separatedBy: .newlines) ?? []
self.tblServices.beginUpdates()
Self.servicesList.append(contentsOf: rawLines.parseIntoCandidateTextServiceStack())
self.tblServices.endUpdates()
default: return
}
}
}
}
// MARK: - TableView Extensions.
extension VwrServiceMenuEditor: NSTableViewDelegate, NSTableViewDataSource {
public func numberOfRows(in _: NSTableView) -> Int {
Self.servicesList.count
}
public func tableView(_: NSTableView, shouldEdit _: NSTableColumn?, row _: Int) -> Bool {
false
}
public func tableView(_: NSTableView, objectValueFor column: NSTableColumn?, row: Int) -> Any? {
defer {
self.btnRemoveService.isEnabled = (0 ..< Self.servicesList.count).contains(
self.tblServices.selectedRow)
}
guard row < Self.servicesList.count else { return "" }
if let column = column {
let colName = column.identifier.rawValue
switch colName {
case "colTitle": return Self.servicesList[row].key
case "colValue": return Self.servicesList[row].definedValue // TODO:
default: return ""
}
}
return Self.servicesList[row]
}
// MARK: Pasteboard Operations.
public func tableView(
_: NSTableView, pasteboardWriterForRow row: Int
) -> NSPasteboardWriting? {
let pasteboard = NSPasteboardItem()
pasteboard.setString(row.description, forType: .string)
return pasteboard
}
public func tableView(
_: NSTableView,
validateDrop _: NSDraggingInfo,
proposedRow _: Int,
proposedDropOperation _: NSTableView.DropOperation
) -> NSDragOperation {
.move
}
public func tableView(
_ tableView: NSTableView,
acceptDrop info: NSDraggingInfo,
row: Int,
dropOperation _: NSTableView.DropOperation
) -> Bool {
var oldIndexes = [Int]()
info.enumerateDraggingItems(
options: [],
for: tableView,
classes: [NSPasteboardItem.self],
searchOptions: [:]
) { dragItem, _, _ in
guard let pasteboardItem = dragItem.item as? NSPasteboardItem else { return }
guard let index = Int(pasteboardItem.string(forType: .string) ?? "NULL"), index >= 0 else { return }
oldIndexes.append(index)
}
var oldIndexOffset = 0
var newIndexOffset = 0
tableView.beginUpdates()
for oldIndex in oldIndexes {
if oldIndex < row {
let contentToMove = Self.servicesList[oldIndex + oldIndexOffset]
Self.servicesList.remove(at: oldIndex + oldIndexOffset)
Self.servicesList.insert(contentToMove, at: row - 1)
tableView.moveRow(at: oldIndex + oldIndexOffset, to: row - 1)
oldIndexOffset -= 1
} else {
let contentToMove = Self.servicesList[oldIndex]
Self.servicesList.remove(at: oldIndex)
Self.servicesList.insert(contentToMove, at: row + newIndexOffset)
tableView.moveRow(at: oldIndex, to: row + newIndexOffset)
newIndexOffset += 1
}
}
tableView.endUpdates()
reassureButtonAvailability()
return true
}
}
// MARK: - Preview.
@available(macOS 14.0, *)
#Preview(traits: .fixedLayout(width: 770, height: 335)) {
VwrServiceMenuEditor()
}

View File

@ -7,10 +7,10 @@
// requirements defined in MIT License. // requirements defined in MIT License.
import CandidateWindow import CandidateWindow
import CocoaExtension
import IMKUtils import IMKUtils
import InputMethodKit import InputMethodKit
import NotifierUI import NotifierUI
import OSFrameworkImpl
import PopupCompositionBuffer import PopupCompositionBuffer
import Shared import Shared
import ShiftKeyUpChecker import ShiftKeyUpChecker
@ -178,7 +178,6 @@ public class SessionCtl: IMKInputController {
// ---------------------------- // ----------------------------
/// ///
inputHandler?.currentLM = inputMode.langModel // inputHandler?.currentLM = inputMode.langModel //
inputHandler?.currentUOM = inputMode.uom
/// ///
inputHandler?.ensureKeyboardParser() inputHandler?.ensureKeyboardParser()
/// ///
@ -214,9 +213,7 @@ public class SessionCtl: IMKInputController {
// //
Self.current?.hidePalettes() Self.current?.hidePalettes()
Self.current = self Self.current = self
self.inputHandler = InputHandler( self.inputHandler = InputHandler(lm: self.inputMode.langModel, pref: PrefMgr.shared)
lm: self.inputMode.langModel, uom: self.inputMode.uom, pref: PrefMgr.shared
)
self.inputHandler?.delegate = self self.inputHandler?.delegate = self
self.syncBaseLMPrefs() self.syncBaseLMPrefs()
// //
@ -313,9 +310,7 @@ public extension SessionCtl {
if self.isActivated { return } if self.isActivated { return }
// setValue() IMK activateServer() setValue() // setValue() IMK activateServer() setValue()
self.inputHandler = InputHandler( self.inputHandler = InputHandler(lm: self.inputMode.langModel, pref: PrefMgr.shared)
lm: self.inputMode.langModel, uom: self.inputMode.uom, pref: PrefMgr.shared
)
self.inputHandler?.delegate = self self.inputHandler?.delegate = self
self.syncBaseLMPrefs() self.syncBaseLMPrefs()

View File

@ -46,8 +46,11 @@ extension SessionCtl: InputHandlerDelegate {
var userPhrase = LMMgr.UserPhrase( var userPhrase = LMMgr.UserPhrase(
keyArray: kvPair.keyArray, value: kvPair.value, inputMode: inputMode keyArray: kvPair.keyArray, value: kvPair.value, inputMode: inputMode
) )
if Self.areWeNerfing { userPhrase.weight = -114.514 } var action = CandidateContextMenuAction.toBoost
LMMgr.writeUserPhrasesAtOnce(userPhrase, areWeFiltering: addToFilter) { if Self.areWeNerfing { action = .toNerf }
if addToFilter { action = .toFilter }
userPhrase.updateWeight(basedOn: action)
LMMgr.writeUserPhrasesAtOnce(userPhrase, areWeFiltering: action == .toFilter) {
succeeded = false succeeded = false
} }
if !succeeded { return false } if !succeeded { return false }
@ -125,6 +128,8 @@ extension SessionCtl: CtlCandidateDelegate {
return shortened ? theEmoji : "\(theEmoji) " + NSLocalizedString("Quick Candidates", comment: "") return shortened ? theEmoji : "\(theEmoji) " + NSLocalizedString("Quick Candidates", comment: "")
} else if PrefMgr.shared.cassetteEnabled { } else if PrefMgr.shared.cassetteEnabled {
return shortened ? "📼" : "📼 " + NSLocalizedString("CIN Cassette Mode", comment: "") return shortened ? "📼" : "📼 " + NSLocalizedString("CIN Cassette Mode", comment: "")
} else if state.type == .ofSymbolTable, state.node.containsCandidateServices {
return shortened ? "🌎" : "🌎 " + NSLocalizedString("Service Menu", comment: "")
} }
return "" return ""
} }
@ -201,6 +206,22 @@ extension SessionCtl: CtlCandidateDelegate {
let node = state.node.members[index] let node = state.node.members[index]
if !node.members.isEmpty { if !node.members.isEmpty {
switchState(IMEState.ofSymbolTable(node: node)) switchState(IMEState.ofSymbolTable(node: node))
} else if let serviceNode = node.asServiceMenuNode {
switch serviceNode.service.value {
case let .url(theURL):
// Safari 使
NSWorkspace.shared.open(theURL)
case .selector:
if let response = serviceNode.service.responseFromSelector {
NSPasteboard.general.declareTypes([.string], owner: nil)
NSPasteboard.general.setString(response, forType: .string)
Notifier.notify(message: "i18n:candidateServiceMenu.selectorResponse.succeeded".localized)
} else {
callError("4DFDC487: Candidate Text Service Selector Responsiveness Failure.")
Notifier.notify(message: "i18n:candidateServiceMenu.selectorResponse.failed".localized)
}
}
switchState(IMEState.ofAbortion())
} else { } else {
switchState(IMEState.ofCommitting(textToCommit: node.name)) switchState(IMEState.ofCommitting(textToCommit: node.name))
} }
@ -257,7 +278,7 @@ extension SessionCtl: CtlCandidateDelegate {
var userPhrase = LMMgr.UserPhrase( var userPhrase = LMMgr.UserPhrase(
keyArray: rawPair.keyArray, value: rawPair.value, inputMode: inputMode keyArray: rawPair.keyArray, value: rawPair.value, inputMode: inputMode
) )
if action == .toNerf { userPhrase.weight = -114.514 } userPhrase.updateWeight(basedOn: action)
LMMgr.writeUserPhrasesAtOnce(userPhrase, areWeFiltering: action == .toFilter) { LMMgr.writeUserPhrasesAtOnce(userPhrase, areWeFiltering: action == .toFilter) {
succeeded = false succeeded = false
} }

View File

@ -79,13 +79,12 @@ public extension SessionCtl {
func showCandidates() { func showCandidates() {
guard client() != nil else { return } guard client() != nil else { return }
updateVerticalTypingStatus() updateVerticalTypingStatus()
isVerticalCandidateWindow = (isVerticalTyping || !PrefMgr.shared.useHorizontalCandidateList) let isServiceMenu = state.type == .ofSymbolTable && state.node.containsCandidateServices
isVerticalCandidateWindow = isVerticalTyping || !PrefMgr.shared.useHorizontalCandidateList
isVerticalCandidateWindow = isVerticalCandidateWindow || isServiceMenu
/// IMK /// IMK
let candidateLayout: NSUserInterfaceLayoutOrientation = let candidateLayout: NSUserInterfaceLayoutOrientation = (isVerticalCandidateWindow ? .vertical : .horizontal)
((isVerticalTyping || !PrefMgr.shared.useHorizontalCandidateList)
? .vertical
: .horizontal)
let isInputtingWithCandidates = state.type == .ofInputting && state.isCandidateContainer let isInputtingWithCandidates = state.type == .ofInputting && state.isCandidateContainer
/// NSWindow() /// NSWindow()
@ -93,6 +92,8 @@ public extension SessionCtl {
candidateUI = CtlCandidateTDK(candidateLayout) candidateUI = CtlCandidateTDK(candidateLayout)
var singleLine = isVerticalTyping || PrefMgr.shared.candidateWindowShowOnlyOneLine var singleLine = isVerticalTyping || PrefMgr.shared.candidateWindowShowOnlyOneLine
singleLine = singleLine || isInputtingWithCandidates singleLine = singleLine || isInputtingWithCandidates
singleLine = singleLine || isServiceMenu
(candidateUI as? CtlCandidateTDK)?.maxLinesPerPage = singleLine ? 1 : 4 (candidateUI as? CtlCandidateTDK)?.maxLinesPerPage = singleLine ? 1 : 4
candidateUI?.candidateFont = Self.candidateFont( candidateUI?.candidateFont = Self.candidateFont(

View File

@ -6,10 +6,10 @@
// marks, or product names of Contributor, except as required to fulfill notice // marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License. // requirements defined in MIT License.
import CocoaExtension
import IMKUtils import IMKUtils
import InputMethodKit import InputMethodKit
import NotifierUI import NotifierUI
import OSFrameworkImpl
import Shared import Shared
import SwiftyCapsLockToggler import SwiftyCapsLockToggler
@ -84,6 +84,7 @@ public extension SessionCtl {
if PrefMgr.shared.shiftEisuToggleOffTogetherWithCapsLock, !isCapsLockTurnedOn, self?.isASCIIMode ?? false { if PrefMgr.shared.shiftEisuToggleOffTogetherWithCapsLock, !isCapsLockTurnedOn, self?.isASCIIMode ?? false {
self?.isASCIIMode.toggle() self?.isASCIIMode.toggle()
} }
self?.resetInputHandler()
guard PrefMgr.shared.showNotificationsWhenTogglingCapsLock else { return } guard PrefMgr.shared.showNotificationsWhenTogglingCapsLock else { return }
guard !PrefMgr.shared.bypassNonAppleCapsLockHandling else { return } guard !PrefMgr.shared.bypassNonAppleCapsLockHandling else { return }
let status = NSLocalizedString("NotificationSwitchRevolver", comment: "") let status = NSLocalizedString("NotificationSwitchRevolver", comment: "")

View File

@ -7,8 +7,8 @@
// requirements defined in MIT License. // requirements defined in MIT License.
import AppKit import AppKit
import CocoaExtension
import NotifierUI import NotifierUI
import OSFrameworkImpl
import Shared import Shared
import SwiftExtension import SwiftExtension
@ -129,6 +129,9 @@ extension SessionCtl {
NSMenu.Item(verbatim: "Client Manager".localized.withEllipsis)? NSMenu.Item(verbatim: "Client Manager".localized.withEllipsis)?
.act(#selector(showClientListMgr(_:))) .act(#selector(showClientListMgr(_:)))
.nulled(silentMode) .nulled(silentMode)
NSMenu.Item(verbatim: "Service Menu Editor".localized.withEllipsis)?
.act(#selector(showServiceMenuEditor(_:)))
.alternated().nulled(silentMode)
NSMenu.Item("Check for Updates…")? NSMenu.Item("Check for Updates…")?
.act(#selector(checkForUpdate(_:))) .act(#selector(checkForUpdate(_:)))
.nulled(silentMode) .nulled(silentMode)
@ -184,6 +187,11 @@ public extension SessionCtl {
NSApp.popup() NSApp.popup()
} }
@objc func showServiceMenuEditor(_: Any? = nil) {
CtlServiceMenuEditor.show()
NSApp.popup()
}
@objc func toggleCassetteMode(_: Any? = nil) { @objc func toggleCassetteMode(_: Any? = nil) {
resetInputHandler(forceComposerCleanup: true) resetInputHandler(forceComposerCleanup: true)
if !PrefMgr.shared.cassetteEnabled, !LMMgr.checkCassettePathValidity(PrefMgr.shared.cassettePath) { if !PrefMgr.shared.cassetteEnabled, !LMMgr.checkCassettePathValidity(PrefMgr.shared.cassettePath) {

View File

@ -33,7 +33,11 @@ public extension SettingsPanesCocoa {
UserDef.kSpecifyShiftBackSpaceKeyBehavior.render(fixWidth: innerContentWidth) UserDef.kSpecifyShiftBackSpaceKeyBehavior.render(fixWidth: innerContentWidth)
UserDef.kSpecifyShiftTabKeyBehavior.render(fixWidth: innerContentWidth) UserDef.kSpecifyShiftTabKeyBehavior.render(fixWidth: innerContentWidth)
UserDef.kSpecifyShiftSpaceKeyBehavior.render(fixWidth: innerContentWidth) UserDef.kSpecifyShiftSpaceKeyBehavior.render(fixWidth: innerContentWidth)
UserDef.kSpecifyCmdOptCtrlEnterBehavior.render(fixWidth: innerContentWidth)
}?.boxed() }?.boxed()
NSView()
}
NSTabView.TabPage(title: "") {
NSStackView.buildSection(width: innerContentWidth) { NSStackView.buildSection(width: innerContentWidth) {
UserDef.kUpperCaseLetterKeyBehavior.render(fixWidth: innerContentWidth) UserDef.kUpperCaseLetterKeyBehavior.render(fixWidth: innerContentWidth)
UserDef.kNumPadCharInputBehavior.render(fixWidth: innerContentWidth) UserDef.kNumPadCharInputBehavior.render(fixWidth: innerContentWidth)
@ -44,7 +48,7 @@ public extension SettingsPanesCocoa {
}?.boxed() }?.boxed()
NSView() NSView()
} }
NSTabView.TabPage(title: "") { NSTabView.TabPage(title: "") {
NSStackView.buildSection(width: innerContentWidth) { NSStackView.buildSection(width: innerContentWidth) {
UserDef.kChooseCandidateUsingSpace.render(fixWidth: innerContentWidth) UserDef.kChooseCandidateUsingSpace.render(fixWidth: innerContentWidth)
UserDef.kEscToCleanInputBuffer.render(fixWidth: innerContentWidth) UserDef.kEscToCleanInputBuffer.render(fixWidth: innerContentWidth)
@ -65,7 +69,7 @@ public extension SettingsPanesCocoa {
}?.boxed() }?.boxed()
NSView() NSView()
} }
NSTabView.TabPage(title: "") { NSTabView.TabPage(title: "") {
NSStackView.buildSection(width: innerContentWidth) { NSStackView.buildSection(width: innerContentWidth) {
UserDef.kBypassNonAppleCapsLockHandling.render(fixWidth: innerContentWidth) UserDef.kBypassNonAppleCapsLockHandling.render(fixWidth: innerContentWidth)
UserDef.kShareAlphanumericalModeStatusAcrossClients.render(fixWidth: innerContentWidth) UserDef.kShareAlphanumericalModeStatusAcrossClients.render(fixWidth: innerContentWidth)

View File

@ -37,6 +37,7 @@ public extension SettingsPanesCocoa {
} }
UserDef.kCandidateWindowShowOnlyOneLine.render(fixWidth: innerContentWidth) UserDef.kCandidateWindowShowOnlyOneLine.render(fixWidth: innerContentWidth)
UserDef.kAlwaysExpandCandidateWindow.render(fixWidth: innerContentWidth) UserDef.kAlwaysExpandCandidateWindow.render(fixWidth: innerContentWidth)
UserDef.kMinCellWidthForHorizontalMatrix.render(fixWidth: innerContentWidth)
UserDef.kRespectClientAccentColor.render(fixWidth: innerContentWidth) UserDef.kRespectClientAccentColor.render(fixWidth: innerContentWidth)
}?.boxed() }?.boxed()
NSView() NSView()
@ -54,10 +55,15 @@ public extension SettingsPanesCocoa {
UserDef.kMoveCursorAfterSelectingCandidate.render(fixWidth: innerContentWidth) UserDef.kMoveCursorAfterSelectingCandidate.render(fixWidth: innerContentWidth)
UserDef.kUseDynamicCandidateWindowOrigin.render(fixWidth: innerContentWidth) UserDef.kUseDynamicCandidateWindowOrigin.render(fixWidth: innerContentWidth)
UserDef.kDodgeInvalidEdgeCandidateCursorPosition.render(fixWidth: innerContentWidth) UserDef.kDodgeInvalidEdgeCandidateCursorPosition.render(fixWidth: innerContentWidth)
UserDef.kUseShiftQuestionToCallServiceMenu
.render(fixWidth: innerContentWidth) { renderable in
renderable.currentControl?.target = self
renderable.currentControl?.action = #selector(self.performCandidateKeysSanityCheck(_:))
}
UserDef.kUseJKtoMoveCompositorCursorInCandidateState UserDef.kUseJKtoMoveCompositorCursorInCandidateState
.render(fixWidth: innerContentWidth) { renderable in .render(fixWidth: innerContentWidth) { renderable in
renderable.currentControl?.target = self renderable.currentControl?.target = self
renderable.currentControl?.action = #selector(self.useJKToMoveBufferCursorDidSet(_:)) renderable.currentControl?.action = #selector(self.performCandidateKeysSanityCheck(_:))
} }
}?.boxed() }?.boxed()
NSView() NSView()
@ -91,7 +97,7 @@ public extension SettingsPanesCocoa {
window.callAlert(title: title.localized, text: explanation.localized) window.callAlert(title: title.localized, text: explanation.localized)
} }
@IBAction func useJKToMoveBufferCursorDidSet(_: NSControl) { @IBAction func performCandidateKeysSanityCheck(_: NSControl) {
// didSet // didSet
PrefMgr.shared.candidateKeys = PrefMgr.shared.candidateKeys PrefMgr.shared.candidateKeys = PrefMgr.shared.candidateKeys
} }

View File

@ -8,8 +8,8 @@
import AppKit import AppKit
import BookmarkManager import BookmarkManager
import CocoaExtension
import Foundation import Foundation
import OSFrameworkImpl
import Shared import Shared
public extension SettingsPanesCocoa { public extension SettingsPanesCocoa {

View File

@ -83,11 +83,6 @@ public extension SettingsPanesCocoa {
SpeechSputnik.shared.refreshStatus() SpeechSputnik.shared.refreshStatus()
} }
@IBAction func updateSCPCSettingsAction(_: NSControl) {
guard PrefMgr.shared.useSCPCTypingMode else { return }
LMMgr.loadSCPCSequencesData()
}
@IBAction func updateUiLanguageAction(_ sender: NSPopUpButton) { @IBAction func updateUiLanguageAction(_ sender: NSPopUpButton) {
let language = languages[sender.indexOfSelectedItem] let language = languages[sender.indexOfSelectedItem]
guard let bundleID = Bundle.main.bundleIdentifier, bundleID.contains("vChewing") else { guard let bundleID = Bundle.main.bundleIdentifier, bundleID.contains("vChewing") else {

View File

@ -140,7 +140,7 @@ extension SettingsPanesCocoa.Phrases: NSTextViewDelegate, NSTextFieldDelegate {
} }
} }
var selUserDataType: vChewingLM.ReplacableUserDataType { var selUserDataType: LMAssembly.ReplacableUserDataType {
switch cmbPEDataTypeMenu.selectedTag() { switch cmbPEDataTypeMenu.selectedTag() {
case 0: return .thePhrases case 0: return .thePhrases
case 1: return .theFilter case 1: return .theFilter
@ -238,7 +238,7 @@ extension SettingsPanesCocoa.Phrases: NSTextViewDelegate, NSTextFieldDelegate {
// NSMenu.items macOS 10.13 // NSMenu.items macOS 10.13
// property 西 // property 西
cmbPEDataTypeMenu.menu?.appendItems { cmbPEDataTypeMenu.menu?.appendItems {
for (tag, neta) in vChewingLM.ReplacableUserDataType.allCases.enumerated() { for (tag, neta) in LMAssembly.ReplacableUserDataType.allCases.enumerated() {
NSMenu.Item(verbatim: neta.localizedDescription)?.tag(tag) NSMenu.Item(verbatim: neta.localizedDescription)?.tag(tag)
} }
} }
@ -332,7 +332,7 @@ extension SettingsPanesCocoa.Phrases: NSTextViewDelegate, NSTextFieldDelegate {
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
guard let self = self else { return } guard let self = self else { return }
self.isLoading = true self.isLoading = true
vChewingLM.LMConsolidator.consolidate(text: &self.tfdPETextEditor.string, pragma: false) LMAssembly.LMConsolidator.consolidate(text: &self.tfdPETextEditor.string, pragma: false)
if self.selUserDataType == .thePhrases { if self.selUserDataType == .thePhrases {
LMMgr.shared.tagOverrides(in: &self.tfdPETextEditor.string, mode: self.selInputMode) LMMgr.shared.tagOverrides(in: &self.tfdPETextEditor.string, mode: self.selInputMode)
} }
@ -416,7 +416,7 @@ private enum PETerminology {
case weightInputBox = case weightInputBox =
"If not filling the weight, it will be 0.0, the maximum one. An ideal weight situates in [-9.5, 0], making itself can be captured by the walking algorithm. The exception is -114.514, the disciplinary weight. The walking algorithm will ignore it unless it is the unique result." "If not filling the weight, it will be 0.0, the maximum one. An ideal weight situates in [-9.5, 0], making itself can be captured by the walking algorithm. The exception is -114.514, the disciplinary weight. The walking algorithm will ignore it unless it is the unique result."
public static func sampleDictionaryContent(for type: vChewingLM.ReplacableUserDataType) -> String { public static func sampleDictionaryContent(for type: LMAssembly.ReplacableUserDataType) -> String {
var result = "" var result = ""
switch type { switch type {
case .thePhrases: case .thePhrases:

View File

@ -35,6 +35,9 @@ public struct VwrSettingsPaneBehavior: View {
@AppStorage(wrappedValue: false, UserDef.kSpecifyShiftSpaceKeyBehavior.rawValue) @AppStorage(wrappedValue: false, UserDef.kSpecifyShiftSpaceKeyBehavior.rawValue)
private var specifyShiftSpaceKeyBehavior: Bool private var specifyShiftSpaceKeyBehavior: Bool
@AppStorage(wrappedValue: 0, UserDef.kSpecifyCmdOptCtrlEnterBehavior.rawValue)
private var specifyCmdOptCtrlEnterBehavior: Int
@AppStorage(wrappedValue: true, UserDef.kUseSpaceToCommitHighlightedSCPCCandidate.rawValue) @AppStorage(wrappedValue: true, UserDef.kUseSpaceToCommitHighlightedSCPCCandidate.rawValue)
private var useSpaceToCommitHighlightedSCPCCandidate: Bool private var useSpaceToCommitHighlightedSCPCCandidate: Bool
@ -80,6 +83,7 @@ public struct VwrSettingsPaneBehavior: View {
UserDef.kSpecifyShiftBackSpaceKeyBehavior.bind($specifyShiftBackSpaceKeyBehavior).render() UserDef.kSpecifyShiftBackSpaceKeyBehavior.bind($specifyShiftBackSpaceKeyBehavior).render()
UserDef.kSpecifyShiftTabKeyBehavior.bind($specifyShiftTabKeyBehavior).render() UserDef.kSpecifyShiftTabKeyBehavior.bind($specifyShiftTabKeyBehavior).render()
.pickerStyle(RadioGroupPickerStyle()) .pickerStyle(RadioGroupPickerStyle())
UserDef.kSpecifyCmdOptCtrlEnterBehavior.bind($specifyCmdOptCtrlEnterBehavior).render()
VStack(alignment: .leading) { VStack(alignment: .leading) {
UserDef.kSpecifyShiftSpaceKeyBehavior.bind($specifyShiftSpaceKeyBehavior).render() UserDef.kSpecifyShiftSpaceKeyBehavior.bind($specifyShiftSpaceKeyBehavior).render()
UserDef.kUseSpaceToCommitHighlightedSCPCCandidate.bind($useSpaceToCommitHighlightedSCPCCandidate).render() UserDef.kUseSpaceToCommitHighlightedSCPCCandidate.bind($useSpaceToCommitHighlightedSCPCCandidate).render()

View File

@ -26,6 +26,9 @@ public struct VwrSettingsPaneCandidates: View {
@AppStorage(wrappedValue: true, UserDef.kRespectClientAccentColor.rawValue) @AppStorage(wrappedValue: true, UserDef.kRespectClientAccentColor.rawValue)
private var respectClientAccentColor: Bool private var respectClientAccentColor: Bool
@AppStorage(wrappedValue: 0, UserDef.kMinCellWidthForHorizontalMatrix.rawValue)
private var minCellWidthForHorizontalMatrix: Int
@AppStorage(wrappedValue: false, UserDef.kAlwaysExpandCandidateWindow.rawValue) @AppStorage(wrappedValue: false, UserDef.kAlwaysExpandCandidateWindow.rawValue)
private var alwaysExpandCandidateWindow: Bool private var alwaysExpandCandidateWindow: Bool
@ -41,6 +44,9 @@ public struct VwrSettingsPaneCandidates: View {
@AppStorage(wrappedValue: false, UserDef.kUseJKtoMoveCompositorCursorInCandidateState.rawValue) @AppStorage(wrappedValue: false, UserDef.kUseJKtoMoveCompositorCursorInCandidateState.rawValue)
private var useJKtoMoveCompositorCursorInCandidateState: Bool private var useJKtoMoveCompositorCursorInCandidateState: Bool
@AppStorage(wrappedValue: true, UserDef.kUseShiftQuestionToCallServiceMenu.rawValue)
public var useShiftQuestionToCallServiceMenu: Bool
@AppStorage(wrappedValue: true, UserDef.kMoveCursorAfterSelectingCandidate.rawValue) @AppStorage(wrappedValue: true, UserDef.kMoveCursorAfterSelectingCandidate.rawValue)
private var moveCursorAfterSelectingCandidate: Bool private var moveCursorAfterSelectingCandidate: Bool
@ -72,6 +78,12 @@ public struct VwrSettingsPaneCandidates: View {
.disabled(useRearCursorMode) .disabled(useRearCursorMode)
} }
UserDef.kDodgeInvalidEdgeCandidateCursorPosition.bind($dodgeInvalidEdgeCandidateCursorPosition).render() UserDef.kDodgeInvalidEdgeCandidateCursorPosition.bind($dodgeInvalidEdgeCandidateCursorPosition).render()
UserDef.kUseShiftQuestionToCallServiceMenu.bind(
$useShiftQuestionToCallServiceMenu.didChange {
// didSet
PrefMgr.shared.candidateKeys = PrefMgr.shared.candidateKeys
}
).render()
UserDef.kUseJKtoMoveCompositorCursorInCandidateState.bind( UserDef.kUseJKtoMoveCompositorCursorInCandidateState.bind(
$useJKtoMoveCompositorCursorInCandidateState.didChange { $useJKtoMoveCompositorCursorInCandidateState.didChange {
// didSet // didSet
@ -93,6 +105,8 @@ public struct VwrSettingsPaneCandidates: View {
if !candidateWindowShowOnlyOneLine { if !candidateWindowShowOnlyOneLine {
UserDef.kAlwaysExpandCandidateWindow.bind($alwaysExpandCandidateWindow).render() UserDef.kAlwaysExpandCandidateWindow.bind($alwaysExpandCandidateWindow).render()
.disabled(candidateWindowShowOnlyOneLine) .disabled(candidateWindowShowOnlyOneLine)
UserDef.kMinCellWidthForHorizontalMatrix.bind($minCellWidthForHorizontalMatrix).render()
.disabled(candidateWindowShowOnlyOneLine)
} }
UserDef.kRespectClientAccentColor.bind($respectClientAccentColor).render() UserDef.kRespectClientAccentColor.bind($respectClientAccentColor).render()
} }

View File

@ -7,8 +7,8 @@
// requirements defined in MIT License. // requirements defined in MIT License.
import BookmarkManager import BookmarkManager
import OSFrameworkImpl
import Shared import Shared
import SwiftExtension
import SwiftUI import SwiftUI
@available(macOS 13, *) @available(macOS 13, *)

View File

@ -7,11 +7,10 @@
// requirements defined in MIT License. // requirements defined in MIT License.
import BookmarkManager import BookmarkManager
import CocoaExtension import OSFrameworkImpl
import Shared import Shared
import SwiftExtension import SwiftExtension
import SwiftUI import SwiftUI
import UniformTypeIdentifiers
@available(macOS 13, *) @available(macOS 13, *)
public struct VwrSettingsPaneDictionary: View { public struct VwrSettingsPaneDictionary: View {

View File

@ -98,12 +98,7 @@ public struct VwrSettingsPaneGeneral: View {
UserDef.kKeepReadingUponCompositionError.bind($keepReadingUponCompositionError).render() UserDef.kKeepReadingUponCompositionError.bind($keepReadingUponCompositionError).render()
UserDef.kClassicHaninKeyboardSymbolModeShortcutEnabled UserDef.kClassicHaninKeyboardSymbolModeShortcutEnabled
.bind($classicHaninKeyboardSymbolModeShortcutEnabled).render() .bind($classicHaninKeyboardSymbolModeShortcutEnabled).render()
UserDef.kUseSCPCTypingMode.bind( UserDef.kUseSCPCTypingMode.bind($useSCPCTypingMode).render()
$useSCPCTypingMode.didChange {
guard useSCPCTypingMode else { return }
LMMgr.loadSCPCSequencesData()
}
).render()
if Date.isTodayTheDate(from: 0401) { if Date.isTodayTheDate(from: 0401) {
UserDef.kShouldNotFartInLieuOfBeep.bind( UserDef.kShouldNotFartInLieuOfBeep.bind(
$shouldNotFartInLieuOfBeep.didChange { onFartControlChange() } $shouldNotFartInLieuOfBeep.didChange { onFartControlChange() }

View File

@ -8,6 +8,7 @@
import AppKit import AppKit
import AVFoundation import AVFoundation
import Shared
public class SpeechSputnik { public class SpeechSputnik {
public static var shared: SpeechSputnik = .init() public static var shared: SpeechSputnik = .init()

View File

@ -0,0 +1,141 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import InputMethodKit
import LangModelAssembly
@testable import MainAssembly
import OSFrameworkImpl
import Shared
import XCTest
class CandidateServiceCoordinatorTests: XCTestCase {
static let testDataMap: [String] = [
#"Unicode Metadata: %s"# + "\t" + #"@SEL:copyUnicodeMetadata:"#,
#"HTML Ruby Zhuyin: %s"# + "\t" + #"@SEL:copyRubyHTMLZhuyinTextbookStyle:"#,
#"HTML Ruby Pinyin: %s"# + "\t" + #"@SEL:copyRubyHTMLHanyuPinyinTextbookStyle:"#,
#"Zhuyin Annotation: %s"# + "\t" + #"@SEL:copyInlineZhuyinAnnotationTextbookStyle:"#,
#"Pinyin Annotation: %s"# + "\t" + #"@SEL:copyInlineHanyuPinyinAnnotationTextbookStyle:"#,
#"Braille 1947: %s"# + "\t" + #"@SEL:copyBraille1947:"#,
#"Braille 2018: %s"# + "\t" + #"@SEL:copyBraille2018:"#,
]
func testSelector_FinalSanityCheck() throws {
var stacked = Self.testDataMap.parseIntoCandidateTextServiceStack(
candidate: "胡桃", reading: [] // 使 Reading
)
let count1 = stacked.count
print("Current Count before Sanity Check ON: \(stacked.count)")
CandidateTextService.enableFinalSanityCheck()
stacked = Self.testDataMap.parseIntoCandidateTextServiceStack(
candidate: "胡桃", reading: [] // 使 Reading
)
let count2 = stacked.count
print("Current Count after Sanity Check ON: \(stacked.count)")
XCTAssertGreaterThan(count1, count2)
}
func testSelector_UnicodeMetadata() throws {
let stacked = Self.testDataMap.parseIntoCandidateTextServiceStack(
candidate: "胡桃", reading: ["ㄏㄨˊ", "ㄊㄠˊ"]
)
let theService = stacked[0]
switch theService.value {
case .url: break
case .selector:
let response = theService.responseFromSelector
let expectedResponse = "胡 U+80E1 CJK UNIFIED IDEOGRAPH-80E1\n桃 U+6843 CJK UNIFIED IDEOGRAPH-6843"
XCTAssertEqual(response, expectedResponse)
}
}
func testSelector_HTMLRubyZhuyinTextbookStyle() throws {
let stacked = Self.testDataMap.parseIntoCandidateTextServiceStack(
candidate: "甜的", reading: ["ㄊㄧㄢˊ", "ㄉㄜ˙"]
)
let theService = stacked[1]
switch theService.value {
case .url: break
case .selector:
let response = theService.responseFromSelector
let expectedResponse = "<ruby>甜<rp>(</rp><rt>ㄊㄧㄢˊ</rt><rp>)</rp></ruby><ruby>的<rp>(</rp><rt>˙ㄉㄜ</rt><rp>)</rp></ruby>"
XCTAssertEqual(response, expectedResponse)
}
}
func testSelector_HTMLRubyPinyinTextbookStyle() throws {
let stacked = Self.testDataMap.parseIntoCandidateTextServiceStack(
candidate: "鐵嘴", reading: ["ㄊㄧㄝˇ", "ㄗㄨㄟˇ"]
)
let theService = stacked[2]
switch theService.value {
case .url: break
case .selector:
let response = theService.responseFromSelector
let expectedResponse = "<ruby>鐵<rp>(</rp><rt>tiě</rt><rp>)</rp></ruby><ruby>嘴<rp>(</rp><rt>zuǐ</rt><rp>)</rp></ruby>"
XCTAssertEqual(response, expectedResponse)
}
}
func testSelector_InlineAnnotationZhuyinTextbookStyle() throws {
let stacked = Self.testDataMap.parseIntoCandidateTextServiceStack(
candidate: "甜的", reading: ["ㄊㄧㄢˊ", "ㄉㄜ˙"]
)
let theService = stacked[3]
switch theService.value {
case .url: break
case .selector:
let response = theService.responseFromSelector
let expectedResponse = "甜(ㄊㄧㄢˊ)的(˙ㄉㄜ)"
XCTAssertEqual(response, expectedResponse)
}
}
func testSelector_InlineAnnotationTextbookStyle() throws {
let stacked = Self.testDataMap.parseIntoCandidateTextServiceStack(
candidate: "鐵嘴", reading: ["ㄊㄧㄝˇ", "ㄗㄨㄟˇ"]
)
let theService = stacked[4]
switch theService.value {
case .url: break
case .selector:
let response = theService.responseFromSelector
let expectedResponse = "鐵(tiě)嘴(zuǐ)"
XCTAssertEqual(response, expectedResponse)
}
}
func testSelector_Braille1947() throws {
let stacked = Self.testDataMap.parseIntoCandidateTextServiceStack(
candidate: "高科技公司的",
reading: ["ㄍㄠ", "ㄎㄜ", "ㄐㄧˋ", "ㄍㄨㄥ", "", "ㄉㄜ˙"]
)
let theService = stacked[5]
switch theService.value {
case .url: break
case .selector:
let response = theService.responseFromSelector
let expectedResponse = "⠅⠩⠄⠇⠮⠄⠅⠡⠐⠅⠯⠄⠑⠄⠙⠮⠁"
XCTAssertEqual(response, expectedResponse)
}
}
func testSelector_Braille2018() throws {
let stacked = Self.testDataMap.parseIntoCandidateTextServiceStack(
candidate: "高科技公司的",
reading: ["ㄍㄠ", "ㄎㄜ", "ㄐㄧˋ", "ㄍㄨㄥ", "", "ㄉㄜ˙"]
)
let theService = stacked[6]
switch theService.value {
case .url: break
case .selector:
let response = theService.responseFromSelector
let expectedResponse = "⠛⠖⠁⠅⠢⠁⠛⠊⠆⠛⠲⠁⠎⠁⠙⠢"
XCTAssertEqual(response, expectedResponse)
}
}
}

View File

@ -6,10 +6,10 @@
// marks, or product names of Contributor, except as required to fulfill notice // marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License. // requirements defined in MIT License.
import CocoaExtension
import InputMethodKit import InputMethodKit
import LangModelAssembly import LangModelAssembly
@testable import MainAssembly @testable import MainAssembly
import OSFrameworkImpl
import Shared import Shared
import XCTest import XCTest
@ -34,16 +34,15 @@ func vCTestLog(_ str: String) {
/// 使 /// 使
/// 使 /// 使
class MainAssemblyTests: XCTestCase { class MainAssemblyTests: XCTestCase {
let testUOM = LangModelAssembly.vChewingLM.LMUserOverride(dataURL: .init(fileURLWithPath: "/dev/null")) var testLM = LMAssembly.LMInstantiator.construct { _ in
var testLM = LangModelAssembly.vChewingLM.LMInstantiator.construct { _ in LMAssembly.LMInstantiator.connectToTestSQLDB()
vChewingLM.LMInstantiator.connectToTestSQLDB()
} }
static let testServer = IMKServer(name: "org.atelierInmu.vChewing.MainAssembly.UnitTests_Connection", bundleIdentifier: "org.atelierInmu.vChewing.MainAssembly.UnitTests") static let testServer = IMKServer(name: "org.atelierInmu.vChewing.MainAssembly.UnitTests_Connection", bundleIdentifier: "org.atelierInmu.vChewing.MainAssembly.UnitTests")
static var _testHandler: InputHandler? static var _testHandler: InputHandler?
var testHandler: InputHandler { var testHandler: InputHandler {
let result = Self._testHandler ?? InputHandler(lm: testLM, uom: testUOM, pref: PrefMgr.shared) let result = Self._testHandler ?? InputHandler(lm: testLM, pref: PrefMgr.shared)
if Self._testHandler == nil { Self._testHandler = result } if Self._testHandler == nil { Self._testHandler = result }
return result return result
} }
@ -65,7 +64,7 @@ class MainAssemblyTests: XCTestCase {
let dataTab = NSEvent.KeyEventData(chars: NSEvent.SpecialKey.tab.unicodeScalar.description, keyCode: KeyCode.kTab.rawValue) let dataTab = NSEvent.KeyEventData(chars: NSEvent.SpecialKey.tab.unicodeScalar.description, keyCode: KeyCode.kTab.rawValue)
func clearTestUOM() { func clearTestUOM() {
testUOM.clearData(withURL: URL(fileURLWithPath: "/dev/null")) testLM.clearUOMData()
} }
func typeSentenceOrCandidates(_ sequence: String) { func typeSentenceOrCandidates(_ sequence: String) {
@ -106,11 +105,11 @@ class MainAssemblyTests: XCTestCase {
} }
} }
extension vChewingLM.LMInstantiator { extension LMAssembly.LMInstantiator {
static func construct( static func construct(
isCHS: Bool = false, completionHandler: @escaping (_ this: vChewingLM.LMInstantiator) -> Void isCHS: Bool = false, completionHandler: @escaping (_ this: LMAssembly.LMInstantiator) -> Void
) -> vChewingLM.LMInstantiator { ) -> LMAssembly.LMInstantiator {
let this = vChewingLM.LMInstantiator(isCHS: isCHS) let this = LMAssembly.LMInstantiator(isCHS: isCHS)
completionHandler(this) completionHandler(this)
return this return this
} }

View File

@ -6,10 +6,10 @@
// marks, or product names of Contributor, except as required to fulfill notice // marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License. // requirements defined in MIT License.
import CocoaExtension
import InputMethodKit import InputMethodKit
import LangModelAssembly import LangModelAssembly
@testable import MainAssembly @testable import MainAssembly
import OSFrameworkImpl
import Shared import Shared
import XCTest import XCTest
@ -151,4 +151,35 @@ extension MainAssemblyTests {
XCTAssertEqual(resultText6, "濟公的年中獎金") XCTAssertEqual(resultText6, "濟公的年中獎金")
vCTestLog("- 已成功證實「年終」的記憶不會對除了給定上下文以外的情形生效。") vCTestLog("- 已成功證實「年終」的記憶不會對除了給定上下文以外的情形生效。")
} }
/// inputHandler.commissionByCtrlOptionCommandEnter()
func test105_InputHandler_MiscCommissionTest() throws {
PrefMgr.shared.useSCPCTypingMode = false
clearTestUOM()
vCTestLog("正在測試 inputHandler.commissionByCtrlOptionCommandEnter()。")
testSession.resetInputHandler(forceComposerCleanup: true)
typeSentenceOrCandidates("el dk ru4ej/ n 2k7")
guard let handler = testSession.inputHandler as? InputHandler else {
XCTAssertThrowsError("testSession.handler is nil.")
return
}
PrefMgr.shared.specifyCmdOptCtrlEnterBehavior = 0
var result = handler.commissionByCtrlOptionCommandEnter(isShiftPressed: true)
XCTAssertEqual(result, "ㄍㄠ ㄎㄜ ㄐㄧˋ ㄍㄨㄥ ㄙ ˙ㄉㄜ")
result = handler.commissionByCtrlOptionCommandEnter() // isShiftPressed false
XCTAssertEqual(result, "高(ㄍㄠ)科(ㄎㄜ)技(ㄐㄧˋ)公(ㄍㄨㄥ)司(ㄙ)的(˙ㄉㄜ)")
PrefMgr.shared.specifyCmdOptCtrlEnterBehavior = 1
result = handler.commissionByCtrlOptionCommandEnter()
let expectedRubyResult = """
<ruby><rp>(</rp><rt></rt><rp>)</rp></ruby><ruby><rp>(</rp><rt></rt><rp>)</rp></ruby><ruby><rp>(</rp><rt>ˋ</rt><rp>)</rp></ruby><ruby><rp>(</rp><rt></rt><rp>)</rp></ruby><ruby><rp>(</rp><rt></rt><rp>)</rp></ruby><ruby><rp>(</rp><rt>˙</rt><rp>)</rp></ruby>
"""
XCTAssertEqual(result, expectedRubyResult)
PrefMgr.shared.specifyCmdOptCtrlEnterBehavior = 2
result = handler.commissionByCtrlOptionCommandEnter()
XCTAssertEqual(result, "⠅⠩⠄⠇⠮⠄⠅⠡⠐⠅⠯⠄⠑⠄⠙⠮⠁")
PrefMgr.shared.specifyCmdOptCtrlEnterBehavior = 3
result = handler.commissionByCtrlOptionCommandEnter()
XCTAssertEqual(result, "⠛⠖⠁⠅⠢⠁⠛⠊⠆⠛⠲⠁⠎⠁⠙⠢")
vCTestLog("成功完成測試 inputHandler.commissionByCtrlOptionCommandEnter()。")
}
} }

View File

@ -13,13 +13,13 @@ let package = Package(
), ),
], ],
dependencies: [ dependencies: [
.package(path: "../vChewing_CocoaExtension"), .package(path: "../vChewing_OSFrameworkImpl"),
], ],
targets: [ targets: [
.target( .target(
name: "NotifierUI", name: "NotifierUI",
dependencies: [ dependencies: [
.product(name: "CocoaExtension", package: "vChewing_CocoaExtension"), .product(name: "OSFrameworkImpl", package: "vChewing_OSFrameworkImpl"),
] ]
), ),
] ]

View File

@ -7,7 +7,7 @@
// requirements defined in MIT License. // requirements defined in MIT License.
import AppKit import AppKit
import CocoaExtension import OSFrameworkImpl
public class Notifier: NSWindowController { public class Notifier: NSWindowController {
public static func notify(message: String) { public static func notify(message: String) {

View File

@ -2,14 +2,14 @@
import PackageDescription import PackageDescription
let package = Package( let package = Package(
name: "CocoaExtension", name: "OSFrameworkImpl",
platforms: [ platforms: [
.macOS(.v11), .macOS(.v11),
], ],
products: [ products: [
.library( .library(
name: "CocoaExtension", name: "OSFrameworkImpl",
targets: ["CocoaExtension"] targets: ["OSFrameworkImpl"]
), ),
], ],
dependencies: [ dependencies: [
@ -17,7 +17,7 @@ let package = Package(
], ],
targets: [ targets: [
.target( .target(
name: "CocoaExtension", name: "OSFrameworkImpl",
dependencies: [ dependencies: [
.product(name: "SwiftExtension", package: "vChewing_SwiftExtension"), .product(name: "SwiftExtension", package: "vChewing_SwiftExtension"),
] ]

View File

@ -1,4 +1,4 @@
# CocoaExtension # OSFrameworkImpl
威注音輸入法針對 Cocoa 的一些功能擴充,使程式維護體驗更佳。 威注音輸入法針對 Cocoa 的一些功能擴充,使程式維護體驗更佳。

View File

@ -71,6 +71,18 @@ public extension NSAttributedString {
public extension NSString { public extension NSString {
var localized: String { NSLocalizedString(description, comment: "") } var localized: String { NSLocalizedString(description, comment: "") }
@objc func getCharDescriptions(_: Any? = nil) -> [String] {
(self as String).charDescriptions
}
@objc func getCodePoints(_: Any? = nil) -> [String] {
(self as String).codePoints
}
@objc func getDescriptionAsCodePoints(_: Any? = nil) -> [String] {
(self as String).describedAsCodePoints
}
} }
// MARK: - NSRange Extension // MARK: - NSRange Extension
@ -128,15 +140,14 @@ public extension NSApplication {
// MARK: - System Dark Mode Status Detector. // MARK: - System Dark Mode Status Detector.
static var isDarkMode: Bool { static var isDarkMode: Bool {
if #unavailable(macOS 10.14) { return false } // "NSApp" can be nil during SPM unit tests.
if #available(macOS 10.15, *) { // Therefore, the method dedicated for macOS 10.15 and later is not considered stable anymore.
let appearanceDescription = NSApp.effectiveAppearance.debugDescription // Fortunately, the method for macOS 10.14 works well on later macOS releases.
.lowercased() if #available(macOS 10.14, *), let strAIS = UserDefaults.current.string(forKey: "AppleInterfaceStyle") {
return appearanceDescription.contains("dark") return strAIS.lowercased().contains("dark")
} else if let appleInterfaceStyle = UserDefaults.current.string(forKey: "AppleInterfaceStyle") { } else {
return appleInterfaceStyle.lowercased().contains("dark") return false
} }
return false
} }
// MARK: - Tell whether this IME is running with Root privileges. // MARK: - Tell whether this IME is running with Root privileges.
@ -199,19 +210,6 @@ public extension NSApplication {
} }
} }
// MARK: - String.applyingTransform
public extension String {
func applyingTransformFW2HW(reverse: Bool) -> String {
if #available(macOS 10.11, *) {
return applyingTransform(.fullwidthToHalfwidth, reverse: reverse) ?? self
}
let theString = NSMutableString(string: self)
CFStringTransform(theString, nil, kCFStringTransformFullwidthHalfwidth, reverse)
return theString as String
}
}
// MARK: - Check whether current date is the given date. // MARK: - Check whether current date is the given date.
public extension Date { public extension Date {
@ -327,3 +325,13 @@ public extension NSApplication {
UserDefaults.standard.object(forKey: "AppleAccentColor") != nil UserDefaults.standard.object(forKey: "AppleAccentColor") != nil
} }
} }
// MARK: - Pasteboard Type Extension.
public extension NSPasteboard.PasteboardType {
static let kUTTypeFileURL = Self(rawValue: "public.file-url") // import UniformTypeIdentifiers
static let kUTTypeData = Self(rawValue: "public.data") // import UniformTypeIdentifiers
static let kUTTypeAppBundle = Self(rawValue: "com.apple.application-bundle") // import UniformTypeIdentifiers
static let kUTTypeUTF8PlainText = Self(rawValue: "public.utf8-plain-text")
static let kNSFilenamesPboardType = Self(rawValue: "NSFilenamesPboardType")
}

View File

@ -460,12 +460,33 @@ public extension NSMenuItem {
var allowedTypes: [String] = ["txt"] var allowedTypes: [String] = ["txt"]
public init() { public convenience init(
_ givenTitle: String? = nil,
target: AnyObject? = nil,
action: Selector? = nil,
postDrag: ((URL) -> Void)? = nil
) {
self.init(
verbatim: givenTitle?.localized,
target: target,
action: action,
postDrag: postDrag
)
}
public init(
verbatim givenTitle: String? = nil,
target: AnyObject? = nil,
action: Selector? = nil,
postDrag: ((URL) -> Void)? = nil
) {
super.init(frame: .zero) super.init(frame: .zero)
bezelStyle = .rounded bezelStyle = .rounded
title = "DRAG FILE TO HERE" title = givenTitle ?? "DRAG FILE TO HERE"
registerForDraggedTypes([.init(rawValue: kUTTypeFileURL as String)]) registerForDraggedTypes([.kUTTypeFileURL])
target = self self.target = target ?? self
self.action = action
postDragHandler = postDrag ?? postDragHandler
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -478,7 +499,7 @@ public extension NSMenuItem {
fileprivate func checkExtension(_ drag: NSDraggingInfo) -> Bool { fileprivate func checkExtension(_ drag: NSDraggingInfo) -> Bool {
guard let pasteboard = drag.draggingPasteboard.propertyList( guard let pasteboard = drag.draggingPasteboard.propertyList(
forType: NSPasteboard.PasteboardType(rawValue: "NSFilenamesPboardType") forType: NSPasteboard.PasteboardType.kNSFilenamesPboardType
) as? [String], let path = pasteboard.first else { ) as? [String], let path = pasteboard.first else {
return false return false
} }
@ -494,7 +515,7 @@ public extension NSMenuItem {
override public func performDragOperation(_ sender: NSDraggingInfo) -> Bool { override public func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
guard let pasteboard = sender.draggingPasteboard.propertyList( guard let pasteboard = sender.draggingPasteboard.propertyList(
forType: NSPasteboard.PasteboardType(rawValue: "NSFilenamesPboardType") forType: NSPasteboard.PasteboardType.kNSFilenamesPboardType
) as? [String], let path = pasteboard.first else { ) as? [String], let path = pasteboard.first else {
print("failure") print("failure")
return false return false

Some files were not shown because too many files have changed in this diff Show More