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)
) 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 journal_mode = OFF;", nil, nil, nil) == SQLITE_OK else { return false }
guard sqlMakeTableMACV.runAsSQLExec(dbPointer: &ptrSQL) else { return false }
@ -231,6 +231,20 @@ func prepareDatabase() -> Bool {
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: -
func rawDictForPhrases(isCHS: Bool) -> [Unigram] {
@ -1044,6 +1058,19 @@ func healthCheck(_ data: [Unigram]) -> String {
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: -
var compileJSON = false
@ -1066,57 +1093,71 @@ func main() {
NSLog("// SQLite 資料庫初期化失敗。")
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 group = DispatchGroup()
group.enter()
globalQueue.async {
NSLog("// 準備編譯符號表情ㄅ文語料檔案。")
commonFileOutput()
taskFlags.remove(.common)
group.leave()
}
group.enter()
globalQueue.async {
NSLog("// 準備編譯繁體中文核心語料檔案。")
fileOutput(isCHS: false)
taskFlags.remove(.cht)
group.leave()
}
group.enter()
globalQueue.async {
NSLog("// 準備編譯簡體中文核心語料檔案。")
fileOutput(isCHS: true)
taskFlags.remove(.chs)
group.leave()
}
//
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()

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,8 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.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 unifiedCharDimension: Double { ceil(unifiedSize * 1.0125 + 7) }
public static var unifiedTextHeight: Double { ceil(unifiedSize * 19 / 16) }
static var internalPrefs = PrefMgr()
public var selectionKey: String
public let displayedText: String
public private(set) var textDimension: NSSize
@ -81,7 +82,8 @@ public class CandidateCellData: Hashable {
}
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 }
return textDimension.width
}

View File

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

View File

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

View File

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

View File

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

View File

@ -40,7 +40,10 @@ extension HotenkaTests {
let testInstance: HotenkaChineseConverter = .init(dictDir: testDataPath)
NSLog("// Loading complete. Generating json dict file.")
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 {
NSLog("// Error on writing strings to file: \(error)")
}

View File

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

View File

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

View File

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

View File

@ -8,9 +8,8 @@
import Foundation
import LineReader
import Shared
public extension vChewingLM {
public extension LMAssembly {
enum LMConsolidator {
public static let kPragmaHeader = "# 𝙵𝙾𝚁𝙼𝙰𝚃 𝚘𝚛𝚐.𝚊𝚝𝚎𝚕𝚒𝚎𝚛𝙸𝚗𝚖𝚞.𝚟𝚌𝚑𝚎𝚠𝚒𝚗𝚐.𝚞𝚜𝚎𝚛𝙻𝚊𝚗𝚐𝚞𝚊𝚐𝚎𝙼𝚘𝚍𝚎𝚕𝙳𝚊𝚝𝚊.𝚏𝚘𝚛𝚖𝚊𝚝𝚝𝚎𝚍"
@ -26,19 +25,19 @@ public extension vChewingLM {
let lineReader = try LineReader(file: fileHandle)
for strLine in lineReader { // i=0
if strLine != kPragmaHeader {
vCLog("Header Mismatch, Starting In-Place Consolidation.")
vCLMLog("Header Mismatch, Starting In-Place Consolidation.")
return false
} else {
vCLog("Header Verification Succeeded: \(strLine).")
vCLMLog("Header Verification Succeeded: \(strLine).")
return true
}
}
} catch {
vCLog("Header Verification Failed: File Access Error.")
vCLMLog("Header Verification Failed: File Access Error.")
return false
}
}
vCLog("Header Verification Failed: File Missing.")
vCLMLog("Header Verification Failed: File Missing.")
return false
}
@ -51,12 +50,12 @@ public extension vChewingLM {
let dict = try FileManager.default.attributesOfItem(atPath: path)
if let value = dict[FileAttributeKey.size] as? UInt64 { fileSize = value }
} catch {
vCLog("EOF Fix Failed: File Missing at \(path).")
vCLMLog("EOF Fix Failed: File Missing at \(path).")
return false
}
guard let fileSize = fileSize else { return false }
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
}
defer { writeFile.closeFile() }
@ -64,11 +63,11 @@ public extension vChewingLM {
/// consolidate()
writeFile.seek(toFileOffset: fileSize - 1)
if writeFile.readDataToEndOfFile().first != 0x0A {
vCLog("EOF Missing Confirmed, Start Fixing.")
vCLMLog("EOF Missing Confirmed, Start Fixing.")
var newData = Data()
newData.append(0x0A)
writeFile.write(newData)
vCLog("EOF Successfully Assured.")
vCLMLog("EOF Successfully Assured.")
}
return false
}
@ -142,14 +141,29 @@ public extension vChewingLM {
// Write consolidated file contents.
try strProcessed.write(to: urlPath, atomically: false, encoding: .utf8)
} catch {
vCLog("Consolidation Failed w/ File: \(path), error: \(error)")
vCLMLog("Consolidation Failed w/ File: \(path), error: \(error)")
return false
}
vCLog("Either Consolidation Successful Or No-Need-To-Consolidate.")
vCLMLog("Either Consolidation Successful Or No-Need-To-Consolidate.")
return true
}
vCLog("Consolidation Failed: File Missing at \(path).")
vCLMLog("Consolidation Failed: File Missing at \(path).")
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 Megrez
import Shared
import SQLite3
public extension vChewingLM {
public extension LMAssembly {
/// LMInstantiatorLMI
/// LangModelProtocol 使
///
@ -43,42 +41,36 @@ public extension vChewingLM {
public var deltaOfCalendarYears: Int = -2000
}
public static var asyncLoadingUserData: Bool = true
// SQLite
static var ptrSQL: OpaquePointer?
// SQLite
public private(set) static var isSQLDBConnected: Bool = false
public internal(set) static var isSQLDBConnected: Bool = false
//
public let isCHS: Bool
//
public var config = Config()
public private(set) var config = Config()
// package
public init(isCHS: Bool = false) {
public init(
isCHS: Bool = false,
uomDataURL: URL? = nil
) {
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)
return self
}
@discardableResult public static func connectSQLDB(dbPath: String, dropPreviousConnection: Bool = true) -> Bool {
if dropPreviousConnection { disconnectSQLDB() }
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
public static func setCassetCandidateKeyValidator(_ validator: @escaping (String) -> Bool) {
Self.lmCassette.candidateKeysValidator = validator
}
///
@ -93,6 +85,7 @@ public extension vChewingLM {
// currentCassetteMetadata
static var lmCassette = LMCassette()
static var lmPlainBopomofo = LMPlainBopomofo()
// 使
// 使使
@ -107,30 +100,46 @@ public extension vChewingLM {
)
var lmReplacements = LMReplacements()
var lmAssociates = LMAssociates()
var lmPlainBopomofo = LMPlainBopomofo()
//
var lmUserOverride: LMUserOverride
// MARK: -
public func resetFactoryJSONModels() {}
public func loadUserPhrasesData(path: String, filterPath: String?) {
DispatchQueue.main.async {
func loadMain() {
if FileManager.default.isReadableFile(atPath: path) {
self.lmUserPhrases.clear()
self.lmUserPhrases.open(path)
vCLog("lmUserPhrases: \(self.lmUserPhrases.count) entries of data loaded from: \(path)")
lmUserPhrases.clear()
lmUserPhrases.open(path)
vCLMLog("lmUserPhrases: \(lmUserPhrases.count) entries of data loaded from: \(path)")
} 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 }
DispatchQueue.main.async {
func loadFilter() {
if FileManager.default.isReadableFile(atPath: filterPath) {
self.lmFiltered.clear()
self.lmFiltered.open(filterPath)
vCLog("lmFiltered: \(self.lmFiltered.count) entries of data loaded from: \(path)")
lmFiltered.clear()
lmFiltered.open(filterPath)
vCLMLog("lmFiltered: \(lmFiltered.count) entries of data loaded from: \(path)")
} 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) {
lmFiltered.clear()
lmFiltered.open(path)
vCLog("lmFiltered: \(lmFiltered.count) entries of data loaded from: \(path)")
vCLMLog("lmFiltered: \(lmFiltered.count) entries of data loaded from: \(path)")
} else {
vCLog("lmFiltered: File access failure: \(path)")
vCLMLog("lmFiltered: File access failure: \(path)")
}
}
public func loadUserSymbolData(path: String) {
DispatchQueue.main.async {
func load() {
if FileManager.default.isReadableFile(atPath: path) {
self.lmUserSymbols.clear()
self.lmUserSymbols.open(path)
vCLog("lmUserSymbol: \(self.lmUserSymbols.count) entries of data loaded from: \(path)")
lmUserSymbols.clear()
lmUserSymbols.open(path)
vCLMLog("lmUserSymbol: \(lmUserSymbols.count) entries of data loaded from: \(path)")
} 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) {
DispatchQueue.main.async {
func load() {
if FileManager.default.isReadableFile(atPath: path) {
self.lmAssociates.clear()
self.lmAssociates.open(path)
vCLog("lmAssociates: \(self.lmAssociates.count) entries of data loaded from: \(path)")
lmAssociates.clear()
lmAssociates.open(path)
vCLMLog("lmAssociates: \(lmAssociates.count) entries of data loaded from: \(path)")
} 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) {
DispatchQueue.main.async {
func load() {
if FileManager.default.isReadableFile(atPath: path) {
self.lmReplacements.clear()
self.lmReplacements.open(path)
vCLog("lmReplacements: \(self.lmReplacements.count) entries of data loaded from: \(path)")
lmReplacements.clear()
lmReplacements.open(path)
vCLMLog("lmReplacements: \(lmReplacements.count) entries of data loaded from: \(path)")
} else {
vCLog("lmReplacements: File access failure: \(path)")
vCLMLog("lmReplacements: File access failure: \(path)")
}
}
}
public func loadSCPCSequencesData() {
let fileName = !isCHS ? "sequenceDataFromEtenDOS-cht" : "sequenceDataFromEtenDOS-chs"
guard let path = Bundle.module.path(forResource: fileName, ofType: "json") else {
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)")
if !Self.asyncLoadingUserData {
load()
} else {
DispatchQueue.main.async {
load()
}
}
}
public var isCassetteDataLoaded: Bool { Self.lmCassette.isLoaded }
public static func loadCassetteData(path: String) {
DispatchQueue.main.async {
func load() {
if FileManager.default.isReadableFile(atPath: path) {
Self.lmCassette.clear()
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 {
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 {
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] == "_" {
// NumPad
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
rawAllUnigrams = rawAllUnigrams.map { unigram in
let convertedValues = unigram.value.parseAsInputToken(isCHS: isCHS)

View File

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

View File

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

View File

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

View File

@ -8,7 +8,6 @@
import Foundation
import Megrez
import Shared
import SQLite3
/* ==============
@ -31,30 +30,49 @@ import SQLite3
) WITHOUT ROWID;
*/
enum CoreColumn: Int32 {
case theDataCHS = 1 //
case theDataCHT = 2 //
case theDataCNS = 3 //
case theDataMISC = 4 //
case theDataSYMB = 5 //
case theDataCHEW = 6 //
extension LMAssembly.LMInstantiator {
enum CoreColumn: Int32 {
case theDataCHS = 1 //
case theDataCHT = 2 //
case theDataCNS = 3 //
case theDataMISC = 4 //
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 {
switch self {
case .theDataCHEW: return -1
case .theDataCNS: return -11
case .theDataSYMB: return -13
case .theDataMISC: return -10
default: return -9.9
var defaultScore: Double {
switch self {
case .theDataCHEW: return -1
case .theDataCNS: return -11
case .theDataSYMB: return -13
case .theDataMISC: return -10
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) {
guard Self.ptrSQL != nil else { return }
performStatementSansResult { ptrStatement in
@ -123,9 +141,10 @@ extension vChewingLM.LMInstantiator {
}
/// UTF8
/// - Remark: 使
/// - parameters:
/// - key:
func factoryCoreUnigramsFor(key: String) -> [Megrez.Unigram] {
public func factoryCoreUnigramsFor(key: String) -> [Megrez.Unigram] {
// ASCII SQLite
factoryUnigramsFor(key: key, column: isCHS ? .theDataCHS : .theDataCHT)
}
@ -134,7 +153,9 @@ extension vChewingLM.LMInstantiator {
/// - parameters:
/// - key:
/// - column:
func factoryUnigramsFor(key: String, column: CoreColumn) -> [Megrez.Unigram] {
func factoryUnigramsFor(
key: String, column: LMAssembly.LMInstantiator.CoreColumn
) -> [Megrez.Unigram] {
if key == "_punctuation_list" { return [] }
var grams: [Megrez.Unigram] = []
var gramsHW: [Megrez.Unigram] = []
@ -142,8 +163,10 @@ extension vChewingLM.LMInstantiator {
let encryptedKey = Self.cnvPhonabetToASCII(key.replacingOccurrences(of: "'", with: "''"))
let sqlQuery = "SELECT * FROM DATA_MAIN WHERE theKey='\(encryptedKey)';"
Self.querySQL(strStmt: sqlQuery, coreColumn: column) { currentResult in
let arrRangeRecords = currentResult.split(separator: "\t")
for strNetaSet in arrRangeRecords {
var i: Double = 0
var previousScore: Double?
currentResult.split(separator: "\t").forEach { strNetaSet in
// stable sort
let neta = Array(strNetaSet.trimmingCharacters(in: .newlines).split(separator: " ").reversed())
let theValue: String = .init(neta[0])
var theScore = column.defaultScore
@ -153,8 +176,15 @@ extension vChewingLM.LMInstantiator {
if theScore > 0 {
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))
if !key.contains("_punctuation") { continue }
if !key.contains("_punctuation") { return }
let halfValue = theValue.applyingTransformFW2HW(reverse: false)
if halfValue != theValue {
gramsHW.append(Megrez.Unigram(value: halfValue, score: theScore))
@ -210,7 +240,7 @@ extension vChewingLM.LMInstantiator {
}
}
private extension vChewingLM.LMInstantiator {
private extension LMAssembly.LMInstantiator {
///
///
/// 使 json
@ -258,7 +288,7 @@ private extension vChewingLM.LMInstantiator {
]
}
public extension vChewingLM.LMInstantiator {
public extension LMAssembly.LMInstantiator {
@discardableResult static func connectToTestSQLDB() -> Bool {
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.
import Megrez
import PinyinPhonaConverter
import Shared
public extension vChewingLM {
@frozen struct LMAssociates {
extension LMAssembly {
struct LMAssociates {
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 = ""
public var count: Int { rangeMap.count }
@ -48,8 +46,8 @@ public extension vChewingLM {
replaceData(textData: rawStrData)
} catch {
filePath = oldPath
vCLog("\(error)")
vCLog("↑ Exception happened when reading data at: \(path).")
vCLMLog("\(error)")
vCLMLog("↑ Exception happened when reading data at: \(path).")
return false
}
@ -93,28 +91,21 @@ public extension vChewingLM {
do {
try strData.write(toFile: filePath, atomically: true, encoding: .utf8)
} catch {
vCLog("Failed to save current database to: \(filePath)")
vCLMLog("Failed to save current database to: \(filePath)")
}
}
public func valuesFor(pair: Megrez.KeyValuePaired) -> [String] {
var pairs: [String] = []
if let arrRangeRecords: [(Range<String.Index>, Int)] = rangeMap[pair.toNGramKey] {
for (netaRange, index) in arrRangeRecords {
let availableResults = [rangeMap[pair.toNGramKey], rangeMap[pair.value]].compactMap { $0 }
availableResults.forEach { arrRangeRecords in
arrRangeRecords.forEach { netaRange, index in
let neta = strData[netaRange].split(separator: " ")
let theValue: String = .init(neta[index])
pairs.append(theValue)
}
}
if let arrRangeRecords: [(Range<String.Index>, Int)] = rangeMap[pair.value] {
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 }
return pairs.deduplicated
}
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 LineReader
import Megrez
import Shared
public extension vChewingLM {
extension LMAssembly {
/// 便使
@frozen struct LMCassette {
struct LMCassette {
public private(set) var filePath: String?
public private(set) var nameShort: 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 supplyQuickResults: Bool = false
public private(set) var supplyPartiallyMatchedResults: Bool = false
public var candidateKeysValidator: (String) -> Bool = { _ in false }
/// 西 - NORM
private var norm = 0.0
}
}
public extension vChewingLM.LMCassette {
extension LMAssembly.LMCassette {
/// 西 - fscale
private static let fscale = 2.7
///
@ -86,7 +86,7 @@ public extension vChewingLM.LMCassette {
if FileManager.default.fileExists(atPath: path) {
do {
guard let fileHandle = FileHandle(forReadingAtPath: path) else {
throw vChewingLM.FileErrors.fileHandleError("")
throw LMAssembly.FileErrors.fileHandleError("")
}
let lineReader = try LineReader(file: fileHandle)
var theMaxKeyLength = 1
@ -195,7 +195,7 @@ public extension vChewingLM.LMCassette {
// Post process.
// Package 便 J / K
//
if CandidateKey.validate(keys: selectionKeys) != nil { selectionKeys = "1234567890" }
if !candidateKeysValidator(selectionKeys) { selectionKeys = "1234567890" }
if !keysUsedInCharDef.intersection(selectionKeys.map(\.description)).isEmpty {
areCandidateKeysShiftHeld = true
}
@ -204,10 +204,10 @@ public extension vChewingLM.LMCassette {
filePath = path
return true
} catch {
vCLog("CIN Loading Failed: File Access Error.")
vCLMLog("CIN Loading Failed: File Access Error.")
}
} else {
vCLog("CIN Loading Failed: File Missing.")
vCLMLog("CIN Loading Failed: File Missing.")
}
filePath = oldPath
return false

View File

@ -7,15 +7,13 @@
// requirements defined in MIT License.
import Megrez
import PinyinPhonaConverter
import Shared
public extension vChewingLM {
extension LMAssembly {
/// LMCore LMCoreEX range
/// range strData
/// C++ ParselessLM Swift
/// For
@frozen struct LMCoreEX {
struct LMCoreEX {
public private(set) var filePath: String?
/// 便 strData
var rangeMap: [String: [Range<String.Index>]] = [:]
@ -81,8 +79,8 @@ public extension vChewingLM {
replaceData(textData: rawStrData)
} catch {
filePath = oldPath
vCLog("\(error)")
vCLog("↑ Exception happened when reading data at: \(path).")
vCLMLog("\(error)")
vCLMLog("↑ Exception happened when reading data at: \(path).")
return false
}
@ -133,7 +131,7 @@ public extension vChewingLM {
}
try dataToWrite.write(toFile: filePath, atomically: true, encoding: .utf8)
} 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
}
}
vCLog(strDump)
vCLMLog(strDump)
}
/// 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.
import Foundation
import Shared
public extension vChewingLM {
@frozen struct LMPlainBopomofo {
public private(set) var filePath: String?
var dataMap: [String: String] = [:]
extension LMAssembly {
struct LMPlainBopomofo {
@usableFromInline typealias DataMap = [String: [String: String]]
let dataMap: DataMap
public var count: Int { dataMap.count }
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 }
@discardableResult public mutating func open(_ path: String) -> Bool {
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] {
public func valuesFor(key: String, isCHS: Bool) -> [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))
}
return pairs.deduplicated
//
return pairs
}
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
// requirements defined in MIT License.
import Shared
public extension vChewingLM {
@frozen struct LMReplacements {
extension LMAssembly {
struct LMReplacements {
public private(set) var filePath: String?
var rangeMap: [String: Range<String.Index>] = [:]
var strData: String = ""
@ -35,8 +33,8 @@ public extension vChewingLM {
replaceData(textData: rawStrData)
} catch {
filePath = oldPath
vCLog("\(error)")
vCLog("↑ Exception happened when reading data at: \(path).")
vCLMLog("\(error)")
vCLMLog("↑ Exception happened when reading data at: \(path).")
return false
}
@ -72,7 +70,7 @@ public extension vChewingLM {
do {
try strData.write(toFile: filePath, atomically: true, encoding: .utf8)
} 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 {
strDump += strData[entry.value] + "\n"
}
vCLog(strDump)
vCLMLog(strDump)
}
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 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 {
// MARK: - Main
var mutCapacity: Int
var mutDecayExponent: Double
var mutLRUList: [KeyObservationPair] = []
var mutLRUMap: [String: KeyObservationPair] = [:]
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 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.
mutDecayExponent = log(0.5) / decayConstant
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
extension vChewingLM.LMUserOverride {
extension LMAssembly.LMUserOverride {
enum OverrideUnit: CodingKey { case count, timestamp, forceHighScoreOverride }
enum ObservationUnit: CodingKey { case count, overrides }
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 {
func bleachSpecifiedSuggestions(targets: [String], saveCallback: @escaping () -> Void) {
extension LMAssembly.LMUserOverride {
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 }
for neta in mutLRUMap {
for target in targets {
@ -166,82 +175,86 @@ public extension vChewingLM.LMUserOverride {
}
}
resetMRUList()
saveCallback()
saveCallback?() ?? saveData()
}
/// LRU
func bleachUnigrams(saveCallback: @escaping () -> Void) {
func bleachUnigrams(saveCallback: (() -> Void)? = nil) {
for key in mutLRUMap.keys {
if !key.contains("(),()") { continue }
mutLRUMap.removeValue(forKey: key)
}
resetMRUList()
saveCallback()
saveCallback?() ?? saveData()
}
internal func resetMRUList() {
func resetMRUList() {
mutLRUList.removeAll()
for neta in mutLRUMap.reversed() {
mutLRUList.append(neta.value)
}
}
func clearData(withURL fileURL: URL) {
func clearData(withURL fileURL: URL? = nil) {
mutLRUMap = .init()
mutLRUList = .init()
do {
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)
} 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
}
}
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
let encoder = JSONEncoder()
do {
guard let jsonData = try? encoder.encode(mutLRUMap) else { return }
let fileURL: URL = fileURL ?? fileSaveLocationURL
try jsonData.write(to: fileURL, options: .atomic)
} catch {
vCLog("UOM Error: Unable to save data, abort saving. Details: \(error)")
vCLMLog("UOM Error: Unable to save data, abort saving. Details: \(error)")
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
let decoder = JSONDecoder()
do {
let data = try Data(contentsOf: fileURL, options: .mappedIfSafe)
if ["", "{}"].contains(String(data: data, encoding: .utf8)) { return }
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
}
mutLRUMap = jsonResult
resetMRUList()
} 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
}
}
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(
key: String, candidate: String, timestamp: Double, forceHighScoreOverride: Bool,
saveCallback: @escaping () -> Void
saveCallback: (() -> Void)?
) {
guard mutLRUMap[key] != nil else {
var observation: Observation = .init()
@ -257,8 +270,8 @@ extension vChewingLM.LMUserOverride {
mutLRUMap.removeValue(forKey: mutLRUList[mutLRUList.endIndex - 1].key)
mutLRUList.removeLast()
}
vCLog("UOM: Observation finished with new observation: \(key)")
saveCallback()
vCLMLog("UOM: Observation finished with new observation: \(key)")
saveCallback?() ?? saveData()
return
}
// decayCallback
@ -268,12 +281,12 @@ extension vChewingLM.LMUserOverride {
)
mutLRUList.insert(theNeta, at: 0)
mutLRUMap[key] = theNeta
vCLog("UOM: Observation finished with existing observation: \(key)")
saveCallback()
vCLMLog("UOM: Observation finished with existing observation: \(key)")
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() }
let observation: Observation = kvPair.observation
var candidates: [(String, Megrez.Unigram)] = .init()
@ -386,3 +399,10 @@ extension vChewingLM.LMUserOverride {
return result
}
}
struct UOMError: LocalizedError {
var rawValue: String
var errorDescription: String? {
NSLocalizedString("rawValue", comment: "")
}
}

View File

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

View File

@ -7,10 +7,9 @@
// requirements defined in MIT License.
import Foundation
import Shared
import SQLite3
public enum vChewingLM {
public enum LMAssembly {
enum FileErrors: Error {
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
}
guard thisResult else {
vCLog("SQL Query Error. Statement: \(strStmt)")
vCLMLog("SQL Query Error. Statement: \(strStmt)")
return false
}
}
@ -83,3 +82,13 @@ func performStatementSansResult(_ handler: (inout OpaquePointer?) -> Void) {
}
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 {
let instance = vChewingLM.LMInstantiator(isCHS: true)
XCTAssertTrue(vChewingLM.LMInstantiator.connectToTestSQLDB())
let instance = LMAssembly.LMInstantiator(isCHS: true)
XCTAssertTrue(LMAssembly.LMInstantiator.connectToTestSQLDB())
instance.setOptions { config in
config.isCNSEnabled = false
config.isSymbolEnabled = false
@ -70,6 +70,6 @@ final class InputTokenTests: XCTestCase {
)
let x = instance.unigramsFor(keyArray: ["ㄐㄧㄣ", "ㄊㄧㄢ", "ㄖˋ", "ㄑㄧˊ"]).description
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 {
func testCassetteLoadWubi86() throws {
let pathCINFile = testDataPath + "wubi.cin"
var lmCassette = vChewingLM.LMCassette()
var lmCassette = LMAssembly.LMCassette()
NSLog("LMCassette: Start loading CIN.")
lmCassette.open(pathCINFile)
NSLog("LMCassette: Finished loading CIN. Entries: \(lmCassette.count)")
@ -41,7 +41,7 @@ final class LMCassetteTests: XCTestCase {
func testCassetteLoadArray30() throws {
let pathCINFile = testDataPath + "array30.cin2"
var lmCassette = vChewingLM.LMCassette()
var lmCassette = LMAssembly.LMCassette()
NSLog("LMCassette: Start loading CIN.")
lmCassette.open(pathCINFile)
NSLog("LMCassette: Finished loading CIN. Entries: \(lmCassette.count)")

View File

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

View File

@ -22,8 +22,8 @@ private let expectedReverseLookupResults: [String] = [
final class LMInstantiatorSQLTests: XCTestCase {
func testSQL() throws {
let instance = vChewingLM.LMInstantiator(isCHS: true)
XCTAssertTrue(vChewingLM.LMInstantiator.connectToTestSQLDB())
let instance = LMAssembly.LMInstantiator(isCHS: true)
XCTAssertTrue(LMAssembly.LMInstantiator.connectToTestSQLDB())
instance.setOptions { config in
config.isCNSEnabled = false
config.isSymbolEnabled = false
@ -41,13 +41,13 @@ final class LMInstantiatorSQLTests: XCTestCase {
XCTAssertEqual(instance.unigramsFor(keyArray: strRefutationKey).count, 10)
XCTAssertEqual(instance.unigramsFor(keyArray: strBoobsKey).last?.description, "(☉☉,-13.0)")
//
XCTAssertEqual(vChewingLM.LMInstantiator.getFactoryReverseLookupData(with: ""), expectedReverseLookupResults)
vChewingLM.LMInstantiator.disconnectSQLDB()
XCTAssertEqual(LMAssembly.LMInstantiator.getFactoryReverseLookupData(with: ""), expectedReverseLookupResults)
LMAssembly.LMInstantiator.disconnectSQLDB()
}
func testCNSMask() throws {
let instance = vChewingLM.LMInstantiator(isCHS: false)
XCTAssertTrue(vChewingLM.LMInstantiator.connectToTestSQLDB())
let instance = LMAssembly.LMInstantiator(isCHS: false)
XCTAssertTrue(LMAssembly.LMInstantiator.connectToTestSQLDB())
instance.setOptions { config in
config.isCNSEnabled = 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")
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: {})
}
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 headReading = "ㄍㄡˇ"
let expectedSuggestion = ""
@ -45,7 +45,7 @@ final class LMUserOverrideTests: XCTestCase {
}
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 headReading = "ㄍㄡˇ"
let valRepeatedlyUsed = "" //
@ -74,7 +74,7 @@ final class LMUserOverrideTests: XCTestCase {
let b = (key: "((ㄆㄞˋ-ㄇㄥˊ,派蒙),(ㄉㄜ˙,的),ㄐㄧㄤˇ-ㄐㄧㄣ)", value: "伙食費", head: "ㄏㄨㄛˇ-ㄕˊ-ㄈㄟˋ")
let c = (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: b.key, candidate: b.value, timestamp: nowTimeStamp + halfLife * 1)
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 {
func testSQL() throws {
let instance = vChewingLM.LMInstantiator(isCHS: true)
let instance = LMAssembly.LMInstantiator(isCHS: true)
instance.setOptions { config in
config.numPadFWHWStatus = nil
}

View File

@ -17,8 +17,9 @@ let package = Package(
.package(path: "../HangarRash_SwiftyCapsLockToggler"),
.package(path: "../Jad_BookmarkManager"),
.package(path: "../Qwertyyb_ShiftKeyUpChecker"),
.package(path: "../vChewing_BrailleSputnik"),
.package(path: "../vChewing_CandidateWindow"),
.package(path: "../vChewing_CocoaExtension"),
.package(path: "../vChewing_OSFrameworkImpl"),
.package(path: "../vChewing_Hotenka"),
.package(path: "../vChewing_IMKUtils"),
.package(path: "../vChewing_KimoDataReader"),
@ -38,9 +39,10 @@ let package = Package(
.target(
name: "MainAssembly",
dependencies: [
.product(name: "BrailleSputnik", package: "vChewing_BrailleSputnik"),
.product(name: "BookmarkManager", package: "Jad_BookmarkManager"),
.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: "Hotenka", package: "vChewing_Hotenka"),
.product(name: "IMKUtils", package: "vChewing_IMKUtils"),

View File

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

View File

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

View File

@ -69,6 +69,8 @@ public extension AppDelegate {
SpeechSputnik.shared.refreshStatus() //
CandidateTextService.enableFinalSanityCheck()
// 使
// Debug
// Debug
@ -145,7 +147,7 @@ public extension AppDelegate {
guard let currentMemorySizeInBytes = NSApplication.memoryFootprint else { return 0 }
let currentMemorySize: Double = (Double(currentMemorySizeInBytes) / 1024 / 1024).rounded(toPlaces: 1)
switch currentMemorySize {
case 384...:
case 1024...:
vCLog("WARNING: EXCESSIVE MEMORY FOOTPRINT (\(currentMemorySize)MB).")
let msgPackage = UNMutableNotificationContent()
msgPackage.title = NSLocalizedString("vChewing", comment: "")
@ -167,4 +169,10 @@ public extension AppDelegate {
}
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.
import Hotenka
import Shared
public enum ChineseConverter {
public static let shared = HotenkaChineseConverter(

View File

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

View File

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

View File

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

View File

@ -9,9 +9,9 @@
/// 調
import CandidateWindow
import CocoaExtension
import InputMethodKit
import Megrez
import OSFrameworkImpl
import Shared
// MARK: - § 調 (Handle Candidate State).
@ -30,6 +30,47 @@ extension InputHandler {
guard ctlCandidate.visible else { return false }
let inputText = ignoringModifiers ? (input.inputTextIgnoringModifiers ?? input.text) : input.text
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: 使
@ -101,7 +142,6 @@ extension InputHandler {
delegate.switchState(IMEState.ofAbortion())
return true
}
let highlightedCandidate = state.candidates[ctlCandidate.highlightedIndex] //
var handleAssociates = !prefs.useSCPCTypingMode && prefs.associatedPhrasesEnabled //
handleAssociates = handleAssociates && compositor.cursor == compositor.length //
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
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`
delegate.callError("172A0F81")

View File

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

View File

@ -393,75 +393,6 @@ extension InputHandler {
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)
/// 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
/// CapsLock
@ -1096,11 +1041,15 @@ extension InputHandler {
let fullWidthResult = behaviorValue % 2 != 0 //
triagePrefs: switch (behaviorValue, isConsideredEmptyForNow) {
case (2, _), (3, _), (4, false), (5, false):
currentLM.config.numPadFWHWStatus = fullWidthResult
currentLM.setOptions { config in
config.numPadFWHWStatus = fullWidthResult
}
if handlePunctuation("_NumPad_\(inputText)") { return true }
default: break triagePrefs // case 0 & 1
}
currentLM.config.numPadFWHWStatus = nil
currentLM.setOptions { config in
config.numPadFWHWStatus = nil
}
delegate.switchState(IMEState.ofEmpty())
let charToCommit = inputText.applyingTransformFW2HW(reverse: fullWidthResult)
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 調
/// 調
import CocoaExtension
import IMKUtils
import InputMethodKit
import LangModelAssembly
import Megrez
import OSFrameworkImpl
import Shared
// MARK: - § 調 (Handle Input with States) * Triage

View File

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

View File

@ -15,26 +15,20 @@ import SwiftExtension
// MARK: - Input Mode Extension for Language Models
public extension Shared.InputMode {
private static let lmCHS = vChewingLM.LMInstantiator(isCHS: true)
private static let lmCHT = vChewingLM.LMInstantiator(isCHS: false)
private static let uomCHS = vChewingLM.LMUserOverride(dataURL: LMMgr.userOverrideModelDataURL(.imeModeCHS))
private static let uomCHT = vChewingLM.LMUserOverride(dataURL: LMMgr.userOverrideModelDataURL(.imeModeCHT))
private static let lmCHS = LMAssembly.LMInstantiator(
isCHS: true, uomDataURL: LMMgr.userOverrideModelDataURL(.imeModeCHS)
)
private static let lmCHT = LMAssembly.LMInstantiator(
isCHS: false, uomDataURL: LMMgr.userOverrideModelDataURL(.imeModeCHT)
)
var langModel: vChewingLM.LMInstantiator {
var langModel: LMAssembly.LMInstantiator {
switch self {
case .imeModeCHS: return Self.lmCHS
case .imeModeCHT: return Self.lmCHT
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.
@ -54,14 +48,14 @@ public class LMMgr {
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) {
guard let path: String = dbPath ?? Self.getCoreDictionaryDBPath() else {
assertionFailure("vChewing factory SQLite data not found.")
return
}
let result = vChewingLM.LMInstantiator.connectSQLDB(dbPath: path)
let result = LMAssembly.LMInstantiator.connectSQLDB(dbPath: path)
assert(result, "vChewing factory SQLite connection failed.")
Notifier.notify(
message: NSLocalizedString("Core Dict loading complete.", comment: "")
@ -71,10 +65,15 @@ public class LMMgr {
///
/// - Remark: cassettePath()
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 {
Shared.InputMode.validCases.forEach { mode in
mode.langModel.loadUserPhrasesData(
@ -82,12 +81,11 @@ public class LMMgr {
filterPath: userDictDataURL(mode: mode, type: .theFilter).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.phraseReplacementEnabled { Self.loadUserPhraseReplacement() }
if PrefMgr.shared.useSCPCTypingMode { Self.loadSCPCSequencesData() }
CandidateNode.load(url: Self.userSymbolMenuDataURL())
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) {
mode.langModel.reloadUserFilterDirectly(path: userDictDataURL(mode: mode, type: .theFilter).path)
}
@ -187,12 +179,12 @@ public class LMMgr {
// MARK: UOM
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()
Shared.InputMode.validCases.forEach { mode in
group.enter()
globalQueue.async {
mode.uom.saveData(toURL: userOverrideModelDataURL(mode))
mode.langModel.saveUOMData()
group.leave()
}
}
@ -201,11 +193,11 @@ public class LMMgr {
}
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) {
mode.uom.bleachUnigrams(saveCallback: { mode.uom.saveData() })
mode.langModel.bleachUOMUnigrams()
}
public static func relocateWreckedUOMData() {
@ -227,6 +219,6 @@ public class LMMgr {
}
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 {
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)
}
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 {
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)
}
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)")
let theURL = Self.userDictDataURL(mode: mode, type: type)
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)
}
@discardableResult public static func saveData(
mode: Shared.InputMode, type: vChewingLM.ReplacableUserDataType, data: String
mode: Shared.InputMode, type: LMAssembly.ReplacableUserDataType, data: String
) -> String {
DispatchQueue.main.async {
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
// requirements defined in MIT License.
import CandidateWindow
import Foundation
import LangModelAssembly
import Megrez
import Shared
// MARK: - 使
@ -32,6 +34,10 @@ public extension LMMgr {
!keyArray.isEmpty && keyArray.filter(\.isEmpty).isEmpty && !value.isEmpty
}
var isSingleCharReadingPair: Bool {
value.count == 1 && keyArray.count == 1 && keyArray.first?.first != "_"
}
public var description: String {
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)
var fileSize: UInt64?
do {
@ -143,7 +149,7 @@ public extension LMMgr {
}
}
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.
var fileSize: UInt64?
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:
/// - type:
/// - 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 = {
switch type {
case .thePhrases: return "userdata"
@ -271,7 +271,7 @@ public extension LMMgr {
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
openPhraseFile(fromURL: userDictDataURL(mode: IMEApp.currentInputMode, type: type), using: app)
guard dual else { return }
@ -324,7 +324,7 @@ public extension LMMgr {
///
///
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)
if !ensureFileExists(userDictDataURL(mode: mode, type: type), deployTemplate: templateName) {
failed = true
@ -334,7 +334,7 @@ public extension LMMgr {
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 {
case .thePhrases: return kTemplateNameUserPhrases
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")
break theLoop
}
let arrResult = vChewingLM.LMInstantiator.getFactoryReverseLookupData(with: char)?.deduplicated ?? []
let arrResult = LMAssembly.LMInstantiator.getFactoryReverseLookupData(with: char)?.deduplicated ?? []
if !arrResult.isEmpty {
strBuilder.append(char + "\t")
strBuilder.append(arrResult.joined(separator: ", "))

View File

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

View File

@ -46,8 +46,11 @@ extension SessionCtl: InputHandlerDelegate {
var userPhrase = LMMgr.UserPhrase(
keyArray: kvPair.keyArray, value: kvPair.value, inputMode: inputMode
)
if Self.areWeNerfing { userPhrase.weight = -114.514 }
LMMgr.writeUserPhrasesAtOnce(userPhrase, areWeFiltering: addToFilter) {
var action = CandidateContextMenuAction.toBoost
if Self.areWeNerfing { action = .toNerf }
if addToFilter { action = .toFilter }
userPhrase.updateWeight(basedOn: action)
LMMgr.writeUserPhrasesAtOnce(userPhrase, areWeFiltering: action == .toFilter) {
succeeded = false
}
if !succeeded { return false }
@ -125,6 +128,8 @@ extension SessionCtl: CtlCandidateDelegate {
return shortened ? theEmoji : "\(theEmoji) " + NSLocalizedString("Quick Candidates", comment: "")
} else if PrefMgr.shared.cassetteEnabled {
return shortened ? "📼" : "📼 " + NSLocalizedString("CIN Cassette Mode", comment: "")
} else if state.type == .ofSymbolTable, state.node.containsCandidateServices {
return shortened ? "🌎" : "🌎 " + NSLocalizedString("Service Menu", comment: "")
}
return ""
}
@ -201,6 +206,22 @@ extension SessionCtl: CtlCandidateDelegate {
let node = state.node.members[index]
if !node.members.isEmpty {
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 {
switchState(IMEState.ofCommitting(textToCommit: node.name))
}
@ -257,7 +278,7 @@ extension SessionCtl: CtlCandidateDelegate {
var userPhrase = LMMgr.UserPhrase(
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) {
succeeded = false
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -83,11 +83,6 @@ public extension SettingsPanesCocoa {
SpeechSputnik.shared.refreshStatus()
}
@IBAction func updateSCPCSettingsAction(_: NSControl) {
guard PrefMgr.shared.useSCPCTypingMode else { return }
LMMgr.loadSCPCSequencesData()
}
@IBAction func updateUiLanguageAction(_ sender: NSPopUpButton) {
let language = languages[sender.indexOfSelectedItem]
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() {
case 0: return .thePhrases
case 1: return .theFilter
@ -238,7 +238,7 @@ extension SettingsPanesCocoa.Phrases: NSTextViewDelegate, NSTextFieldDelegate {
// NSMenu.items macOS 10.13
// property 西
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)
}
}
@ -332,7 +332,7 @@ extension SettingsPanesCocoa.Phrases: NSTextViewDelegate, NSTextFieldDelegate {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
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 {
LMMgr.shared.tagOverrides(in: &self.tfdPETextEditor.string, mode: self.selInputMode)
}
@ -416,7 +416,7 @@ private enum PETerminology {
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."
public static func sampleDictionaryContent(for type: vChewingLM.ReplacableUserDataType) -> String {
public static func sampleDictionaryContent(for type: LMAssembly.ReplacableUserDataType) -> String {
var result = ""
switch type {
case .thePhrases:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,7 @@
import AppKit
import AVFoundation
import Shared
public class SpeechSputnik {
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
// requirements defined in MIT License.
import CocoaExtension
import InputMethodKit
import LangModelAssembly
@testable import MainAssembly
import OSFrameworkImpl
import Shared
import XCTest
@ -34,16 +34,15 @@ func vCTestLog(_ str: String) {
/// 使
/// 使
class MainAssemblyTests: XCTestCase {
let testUOM = LangModelAssembly.vChewingLM.LMUserOverride(dataURL: .init(fileURLWithPath: "/dev/null"))
var testLM = LangModelAssembly.vChewingLM.LMInstantiator.construct { _ in
vChewingLM.LMInstantiator.connectToTestSQLDB()
var testLM = LMAssembly.LMInstantiator.construct { _ in
LMAssembly.LMInstantiator.connectToTestSQLDB()
}
static let testServer = IMKServer(name: "org.atelierInmu.vChewing.MainAssembly.UnitTests_Connection", bundleIdentifier: "org.atelierInmu.vChewing.MainAssembly.UnitTests")
static 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 }
return result
}
@ -65,7 +64,7 @@ class MainAssemblyTests: XCTestCase {
let dataTab = NSEvent.KeyEventData(chars: NSEvent.SpecialKey.tab.unicodeScalar.description, keyCode: KeyCode.kTab.rawValue)
func clearTestUOM() {
testUOM.clearData(withURL: URL(fileURLWithPath: "/dev/null"))
testLM.clearUOMData()
}
func typeSentenceOrCandidates(_ sequence: String) {
@ -106,11 +105,11 @@ class MainAssemblyTests: XCTestCase {
}
}
extension vChewingLM.LMInstantiator {
extension LMAssembly.LMInstantiator {
static func construct(
isCHS: Bool = false, completionHandler: @escaping (_ this: vChewingLM.LMInstantiator) -> Void
) -> vChewingLM.LMInstantiator {
let this = vChewingLM.LMInstantiator(isCHS: isCHS)
isCHS: Bool = false, completionHandler: @escaping (_ this: LMAssembly.LMInstantiator) -> Void
) -> LMAssembly.LMInstantiator {
let this = LMAssembly.LMInstantiator(isCHS: isCHS)
completionHandler(this)
return this
}

View File

@ -6,10 +6,10 @@
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import CocoaExtension
import InputMethodKit
import LangModelAssembly
@testable import MainAssembly
import OSFrameworkImpl
import Shared
import XCTest
@ -151,4 +151,35 @@ extension MainAssemblyTests {
XCTAssertEqual(resultText6, "濟公的年中獎金")
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: [
.package(path: "../vChewing_CocoaExtension"),
.package(path: "../vChewing_OSFrameworkImpl"),
],
targets: [
.target(
name: "NotifierUI",
dependencies: [
.product(name: "CocoaExtension", package: "vChewing_CocoaExtension"),
.product(name: "OSFrameworkImpl", package: "vChewing_OSFrameworkImpl"),
]
),
]

View File

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

View File

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

View File

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

View File

@ -71,6 +71,18 @@ public extension NSAttributedString {
public extension NSString {
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
@ -128,15 +140,14 @@ public extension NSApplication {
// MARK: - System Dark Mode Status Detector.
static var isDarkMode: Bool {
if #unavailable(macOS 10.14) { return false }
if #available(macOS 10.15, *) {
let appearanceDescription = NSApp.effectiveAppearance.debugDescription
.lowercased()
return appearanceDescription.contains("dark")
} else if let appleInterfaceStyle = UserDefaults.current.string(forKey: "AppleInterfaceStyle") {
return appleInterfaceStyle.lowercased().contains("dark")
// "NSApp" can be nil during SPM unit tests.
// Therefore, the method dedicated for macOS 10.15 and later is not considered stable anymore.
// Fortunately, the method for macOS 10.14 works well on later macOS releases.
if #available(macOS 10.14, *), let strAIS = UserDefaults.current.string(forKey: "AppleInterfaceStyle") {
return strAIS.lowercased().contains("dark")
} else {
return false
}
return false
}
// 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.
public extension Date {
@ -327,3 +325,13 @@ public extension NSApplication {
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"]
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)
bezelStyle = .rounded
title = "DRAG FILE TO HERE"
registerForDraggedTypes([.init(rawValue: kUTTypeFileURL as String)])
target = self
title = givenTitle ?? "DRAG FILE TO HERE"
registerForDraggedTypes([.kUTTypeFileURL])
self.target = target ?? self
self.action = action
postDragHandler = postDrag ?? postDragHandler
}
required init?(coder: NSCoder) {
@ -478,7 +499,7 @@ public extension NSMenuItem {
fileprivate func checkExtension(_ drag: NSDraggingInfo) -> Bool {
guard let pasteboard = drag.draggingPasteboard.propertyList(
forType: NSPasteboard.PasteboardType(rawValue: "NSFilenamesPboardType")
forType: NSPasteboard.PasteboardType.kNSFilenamesPboardType
) as? [String], let path = pasteboard.first else {
return false
}
@ -494,7 +515,7 @@ public extension NSMenuItem {
override public func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
guard let pasteboard = sender.draggingPasteboard.propertyList(
forType: NSPasteboard.PasteboardType(rawValue: "NSFilenamesPboardType")
forType: NSPasteboard.PasteboardType.kNSFilenamesPboardType
) as? [String], let path = pasteboard.first else {
print("failure")
return false

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