Compare commits
No commits in common. "main" and "3.7.3" have entirely different histories.
|
@ -1,4 +1,4 @@
|
||||||
name: debug-macOS-MainAssembly
|
name: Build-with-macOS-latest
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "main" ]
|
branches: [ "main" ]
|
||||||
|
@ -17,6 +17,6 @@ jobs:
|
||||||
xcode-version: '^15.1'
|
xcode-version: '^15.1'
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
- name: Clean
|
- name: Clean
|
||||||
run: make spmClean
|
run: make clean
|
||||||
- name: Build
|
- name: Build
|
||||||
run: make spmDebug
|
run: git pull --all && git submodule sync; make update; make
|
||||||
|
|
|
@ -194,7 +194,7 @@ func prepareDatabase() -> Bool {
|
||||||
PRIMARY KEY (theChar)
|
PRIMARY KEY (theChar)
|
||||||
) WITHOUT ROWID;
|
) WITHOUT ROWID;
|
||||||
"""
|
"""
|
||||||
guard sqlite3_open(":memory:", &ptrSQL) == SQLITE_OK else { return false }
|
guard sqlite3_open(urlSQLite, &ptrSQL) == SQLITE_OK else { return false }
|
||||||
guard sqlite3_exec(ptrSQL, "PRAGMA synchronous = OFF;", nil, nil, nil) == SQLITE_OK else { return false }
|
guard sqlite3_exec(ptrSQL, "PRAGMA synchronous = OFF;", nil, nil, nil) == SQLITE_OK else { return false }
|
||||||
guard sqlite3_exec(ptrSQL, "PRAGMA journal_mode = OFF;", nil, nil, nil) == SQLITE_OK else { return false }
|
guard sqlite3_exec(ptrSQL, "PRAGMA journal_mode = OFF;", nil, nil, nil) == SQLITE_OK else { return false }
|
||||||
guard sqlMakeTableMACV.runAsSQLExec(dbPointer: &ptrSQL) else { return false }
|
guard sqlMakeTableMACV.runAsSQLExec(dbPointer: &ptrSQL) else { return false }
|
||||||
|
@ -231,20 +231,6 @@ func prepareDatabase() -> Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Dump SQLite3 Memory Database to File.
|
|
||||||
|
|
||||||
@discardableResult func dumpSQLDB() -> Bool {
|
|
||||||
var ptrSQLTarget: OpaquePointer?
|
|
||||||
defer { sqlite3_close_v2(ptrSQLTarget) }
|
|
||||||
guard sqlite3_open(urlSQLite, &ptrSQLTarget) == SQLITE_OK else { return false }
|
|
||||||
let ptrBackupObj = sqlite3_backup_init(ptrSQLTarget, "main", ptrSQL, "main")
|
|
||||||
if ptrBackupObj != nil {
|
|
||||||
sqlite3_backup_step(ptrBackupObj, -1)
|
|
||||||
sqlite3_backup_finish(ptrBackupObj)
|
|
||||||
}
|
|
||||||
return sqlite3_errcode(ptrSQLTarget) == SQLITE_OK
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 載入詞組檔案且輸出陣列
|
// MARK: - 載入詞組檔案且輸出陣列
|
||||||
|
|
||||||
func rawDictForPhrases(isCHS: Bool) -> [Unigram] {
|
func rawDictForPhrases(isCHS: Bool) -> [Unigram] {
|
||||||
|
@ -1058,19 +1044,6 @@ func healthCheck(_ data: [Unigram]) -> String {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 與主執行緒有關的任務 Flags
|
|
||||||
|
|
||||||
struct TaskFlags: OptionSet {
|
|
||||||
public let rawValue: Int
|
|
||||||
public init(rawValue: Int) {
|
|
||||||
self.rawValue = rawValue
|
|
||||||
}
|
|
||||||
|
|
||||||
public static let common = TaskFlags(rawValue: 1 << 0)
|
|
||||||
public static let chs = TaskFlags(rawValue: 1 << 1)
|
|
||||||
public static let cht = TaskFlags(rawValue: 1 << 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 主執行緒
|
// MARK: - 主執行緒
|
||||||
|
|
||||||
var compileJSON = false
|
var compileJSON = false
|
||||||
|
@ -1093,10 +1066,28 @@ func main() {
|
||||||
NSLog("// SQLite 資料庫初期化失敗。")
|
NSLog("// SQLite 資料庫初期化失敗。")
|
||||||
exit(-1)
|
exit(-1)
|
||||||
}
|
}
|
||||||
|
let globalQueue = DispatchQueue.global(qos: .default)
|
||||||
var taskFlags: TaskFlags = [.common, .chs, .cht] {
|
let group = DispatchGroup()
|
||||||
didSet {
|
group.enter()
|
||||||
guard taskFlags.isEmpty else { return }
|
globalQueue.async {
|
||||||
|
NSLog("// 準備編譯符號表情ㄅ文語料檔案。")
|
||||||
|
commonFileOutput()
|
||||||
|
group.leave()
|
||||||
|
}
|
||||||
|
group.enter()
|
||||||
|
globalQueue.async {
|
||||||
|
NSLog("// 準備編譯繁體中文核心語料檔案。")
|
||||||
|
fileOutput(isCHS: false)
|
||||||
|
group.leave()
|
||||||
|
}
|
||||||
|
group.enter()
|
||||||
|
globalQueue.async {
|
||||||
|
NSLog("// 準備編譯簡體中文核心語料檔案。")
|
||||||
|
fileOutput(isCHS: true)
|
||||||
|
group.leave()
|
||||||
|
}
|
||||||
|
// 一直等待完成
|
||||||
|
group.wait()
|
||||||
NSLog("// 全部 TXT 辭典檔案建置完畢。")
|
NSLog("// 全部 TXT 辭典檔案建置完畢。")
|
||||||
if compileJSON {
|
if compileJSON {
|
||||||
NSLog("// 全部 JSON 辭典檔案建置完畢。")
|
NSLog("// 全部 JSON 辭典檔案建置完畢。")
|
||||||
|
@ -1123,41 +1114,9 @@ func main() {
|
||||||
assert(committed)
|
assert(committed)
|
||||||
let compressed = "VACUUM;".runAsSQLExec(dbPointer: &ptrSQL)
|
let compressed = "VACUUM;".runAsSQLExec(dbPointer: &ptrSQL)
|
||||||
assert(compressed)
|
assert(compressed)
|
||||||
if !dumpSQLDB() {
|
sqlite3_close_v2(ptrSQL)
|
||||||
NSLog("// SQLite 辭典傾印失敗。")
|
|
||||||
} else {
|
|
||||||
NSLog("// 全部 SQLite 辭典檔案建置完畢。")
|
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -50,7 +50,7 @@ var allRegisteredInstancesOfThisInputMethod: [TISInputSource] {
|
||||||
else {
|
else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
return TISInputSource.match(modeIDs: tsInputModeListKey.keys.map(\.description))
|
return tsInputModeListKey.keys.compactMap { TISInputSource.generate(from: $0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - NSApp Activation Helper
|
// MARK: - NSApp Activation Helper
|
||||||
|
|
|
@ -10,8 +10,6 @@ import AppKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
public struct MainView: View {
|
public struct MainView: View {
|
||||||
static let strCopyrightLabel = Bundle.main.localizedInfoDictionary?["NSHumanReadableCopyright"] as? String ?? "BAD_COPYRIGHT_LABEL"
|
|
||||||
|
|
||||||
@State var pendingSheetPresenting = false
|
@State var pendingSheetPresenting = false
|
||||||
@State var isShowingAlertForFailedInstallation = false
|
@State var isShowingAlertForFailedInstallation = false
|
||||||
@State var isShowingAlertForMissingPostInstall = false
|
@State var isShowingAlertForMissingPostInstall = false
|
||||||
|
@ -55,7 +53,6 @@ public struct MainView: View {
|
||||||
Text("v\(versionString) Build \(installingVersion)").lineLimit(1)
|
Text("v\(versionString) Build \(installingVersion)").lineLimit(1)
|
||||||
}.fixedSize()
|
}.fixedSize()
|
||||||
Text("i18n:installer.APP_DERIVED_FROM").font(.custom("Tahoma", size: 11))
|
Text("i18n:installer.APP_DERIVED_FROM").font(.custom("Tahoma", size: 11))
|
||||||
Text(Self.strCopyrightLabel).font(.custom("Tahoma", size: 11))
|
|
||||||
Text("i18n:installer.DEV_CREW").font(.custom("Tahoma", size: 11)).padding([.vertical], 2)
|
Text("i18n:installer.DEV_CREW").font(.custom("Tahoma", size: 11)).padding([.vertical], 2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,30 @@
|
||||||
"Abort" = "Abort";
|
"vChewing Input Method" = "vChewing Input Method";
|
||||||
"Attention" = "Attention";
|
"i18n:installer.DO_APP_UPGRADE" = "Accept & Upgrade";
|
||||||
"Cancel" = "Cancel";
|
"Cancel" = "Cancel";
|
||||||
"Cannot activate the input method." = "Cannot activate the input method.";
|
"Cannot activate the input method." = "Cannot activate the input method.";
|
||||||
"Cannot copy the file to the destination." = "Cannot copy the file to the destination.";
|
"Cannot copy the file to the destination." = "Cannot copy the file to the destination.";
|
||||||
"Cannot find input source %@ after registration." = "Cannot find input source %@ after registration.";
|
|
||||||
"Cannot register input source %@ at %@." = "Cannot register input source %@ at %@.";
|
|
||||||
"Continue" = "Continue";
|
|
||||||
"Fatal Error" = "Fatal Error";
|
|
||||||
"i18n:installer.ACCEPT_INSTALLATION" = "I Accept";
|
|
||||||
"i18n:installer.APP_DERIVED_FROM" = "Was derived from OpenVanilla McBopopmofo Project (MIT-License).";
|
|
||||||
"i18n:installer.APP_NAME" = "vChewing for macOS";
|
|
||||||
"i18n:installer.CANCEL_INSTALLATION" = "Cancel";
|
|
||||||
"i18n:installer.DEV_CREW" = "vChewing macOS Development: Shiki Suen, Isaac Xen, Hiraku Wang, etc.\nvChewing Phrase Database Maintained by Shiki Suen.\nWalking algorithm by Lukhnos Liu (from Gramambular 2, MIT-License).";
|
|
||||||
"i18n:installer.DISCLAIMER_TEXT" = "DISCLAIMER: The vChewing project, having no relationship of cooperation or affiliation with the OpenVanilla project, is not responsible for the phrase database shipped in the original McBopomofo project. Certain geopolitical and ideological contents, which are potentially harmful to the global spread of this software, are not included in vChewing official phrase database.";
|
|
||||||
"i18n:installer.DO_APP_UPGRADE" = "Accept & Upgrade";
|
|
||||||
"i18n:installer.EULA_PROMPT_NOTICE" = "By installing the software, you must accept the terms above.";
|
|
||||||
"i18n:installer.INSTALLER_APP_TITLE_FULL" = "vChewing Installer";
|
|
||||||
"i18n:installer.INSTALLER_APP_TITLE" = "vChewing Installer";
|
|
||||||
"i18n:installer.LICENSE_TITLE" = "MIT-NTL License:";
|
|
||||||
"i18n:installer.STOPPING_THE_OLD_VERSION" = "Stopping the old version. This may take up to one minute…";
|
|
||||||
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." = "Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources.";
|
|
||||||
"Install Failed" = "Install Failed";
|
"Install Failed" = "Install Failed";
|
||||||
"Installation Successful" = "Installation Successful";
|
"Installation Successful" = "Installation Successful";
|
||||||
"OK" = "OK";
|
"OK" = "OK";
|
||||||
"Stopping the old version. This may take up to one minute…" = "Stopping the old version. This may take up to one minute…";
|
|
||||||
"vChewing Input Method" = "vChewing Input Method";
|
|
||||||
"vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account." = "vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account.";
|
"vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account." = "vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account.";
|
||||||
|
"Stopping the old version. This may take up to one minute…" = "Stopping the old version. This may take up to one minute…";
|
||||||
|
"Attention" = "Attention";
|
||||||
"vChewing is upgraded, but please log out or reboot for the new version to be fully functional." = "vChewing is upgraded, but please log out or reboot for the new version to be fully functional.";
|
"vChewing is upgraded, but please log out or reboot for the new version to be fully functional." = "vChewing is upgraded, but please log out or reboot for the new version to be fully functional.";
|
||||||
|
"Fatal Error" = "Fatal Error";
|
||||||
|
"Abort" = "Abort";
|
||||||
|
"Cannot register input source %@ at %@." = "Cannot register input source %@ at %@.";
|
||||||
|
"Cannot find input source %@ after registration." = "Cannot find input source %@ after registration.";
|
||||||
"Warning" = "Warning";
|
"Warning" = "Warning";
|
||||||
|
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." = "Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources.";
|
||||||
|
"Continue" = "Continue";
|
||||||
|
"i18n:installer.DISCLAIMER_TEXT" = "DISCLAIMER: The vChewing project, having no relationship of cooperation or affiliation with the OpenVanilla project, is not responsible for the phrase database shipped in the original McBopomofo project. Certain geopolitical and ideological contents, which are potentially harmful to the global spread of this software, are not included in vChewing official phrase database.";
|
||||||
|
"i18n:installer.INSTALLER_APP_TITLE" = "vChewing Installer";
|
||||||
|
"i18n:installer.INSTALLER_APP_TITLE_FULL" = "vChewing Installer";
|
||||||
|
"i18n:installer.ACCEPT_INSTALLATION" = "I Accept";
|
||||||
|
"i18n:installer.CANCEL_INSTALLATION" = "Cancel";
|
||||||
|
"i18n:installer.LICENSE_TITLE" = "MIT-NTL License:";
|
||||||
|
"i18n:installer.APP_NAME" = "vChewing for macOS";
|
||||||
|
"i18n:installer.APP_DERIVED_FROM" = "Was derived from OpenVanilla McBopopmofo Project (MIT-License).";
|
||||||
|
"i18n:installer.DEV_CREW" = "vChewing macOS Development: Shiki Suen, Isaac Xen, Hiraku Wang, etc.\nvChewing Phrase Database Maintained by Shiki Suen.\nWalking algorithm by Lukhnos Liu (from Gramambular 2, MIT-License).";
|
||||||
|
"i18n:installer.EULA_PROMPT_NOTICE" = "By installing the software, you must accept the terms above.";
|
||||||
|
"i18n:installer.STOPPING_THE_OLD_VERSION" = "Stopping the old version. This may take up to one minute…";
|
||||||
|
|
|
@ -1,30 +1,30 @@
|
||||||
"Abort" = "中止";
|
"vChewing Input Method" = "威注音入力アプリ";
|
||||||
"Attention" = "ご注意";
|
"i18n:installer.DO_APP_UPGRADE" = "承認と更新";
|
||||||
"Cancel" = "取消";
|
"Cancel" = "取消";
|
||||||
"Cannot activate the input method." = "入力アプリ、起動失敗。";
|
"Cannot activate the input method." = "入力アプリ、起動失敗。";
|
||||||
"Cannot copy the file to the destination." = "目標へファイルのコピーできません。";
|
"Cannot copy the file to the destination." = "目標へファイルのコピーできません。";
|
||||||
"Cannot find input source %@ after registration." = "登録済みですが「%@」は見つけませんでした。";
|
|
||||||
"Cannot register input source %@ at %@." = "「%2$@」で入力アプリ「\"%1$@\"」の実装は失敗しました。";
|
|
||||||
"Continue" = "続行";
|
|
||||||
"Fatal Error" = "致命錯乱";
|
|
||||||
"i18n:installer.ACCEPT_INSTALLATION" = "承認する";
|
|
||||||
"i18n:installer.APP_DERIVED_FROM" = "曾て OpenVanilla 小麦注音プロジェクト (MIT-License) から派生。";
|
|
||||||
"i18n:installer.APP_NAME" = "vChewing for macOS";
|
|
||||||
"i18n:installer.CANCEL_INSTALLATION" = "取消";
|
|
||||||
"i18n:installer.DEV_CREW" = "macOS 版威注音の開発:Shiki Suen, Isaac Xen, Hiraku Wang, など。\n威注音語彙データの維持:Shiki Suen。\nウォーキング算法:Lukhnos Liu (Gramambular 2, MIT-License)。";
|
|
||||||
"i18n:installer.DISCLAIMER_TEXT" = "免責事項:vChewing Project は、OpenVanilla と協力関係や提携関係にあるわけではなく、OpenVanilla が小麦注音プロジェクトに同梱した辞書データについて、vChewing Project は一切責任負い兼ねる。特定な地政学的・観念形態的な内容は、vChewing アプリの世界的な普及に妨害する恐れがあるため、vChewing 公式辞書データに不収録。";
|
|
||||||
"i18n:installer.DO_APP_UPGRADE" = "承認と更新";
|
|
||||||
"i18n:installer.EULA_PROMPT_NOTICE" = "このアプリを実装するために、上記の条約を承認すべきである。";
|
|
||||||
"i18n:installer.INSTALLER_APP_TITLE_FULL" = "威注音入力 実装用アプリ";
|
|
||||||
"i18n:installer.INSTALLER_APP_TITLE" = "威注音入力 実装用アプリ";
|
|
||||||
"i18n:installer.LICENSE_TITLE" = "MIT商標不許可ライセンス (MIT-NTL License):";
|
|
||||||
"i18n:installer.STOPPING_THE_OLD_VERSION" = "古いバージョンを強制停止中。1分かかると恐れ入りますが……";
|
|
||||||
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." = "入力アプリの自動起動はうまく出来なかったかもしれません。ご自分で「システム環境設定→キーボード→入力ソース」で起動してください。";
|
|
||||||
"Install Failed" = "実装失敗。";
|
"Install Failed" = "実装失敗。";
|
||||||
"Installation Successful" = "実装完了";
|
"Installation Successful" = "実装完了";
|
||||||
"OK" = "うむ";
|
"OK" = "うむ";
|
||||||
"Stopping the old version. This may take up to one minute…" = "古いバージョンを強制停止中。1分かかると恐れ入りますが……";
|
|
||||||
"vChewing Input Method" = "威注音入力アプリ";
|
|
||||||
"vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account." = "威注音入力、利用準備完了。\n\nこのシステムユーザーアカウントで初めて実装した場合、再ログインしてください。";
|
"vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account." = "威注音入力、利用準備完了。\n\nこのシステムユーザーアカウントで初めて実装した場合、再ログインしてください。";
|
||||||
|
"Stopping the old version. This may take up to one minute…" = "古いバージョンを強制停止中。1分かかると恐れ入りますが……";
|
||||||
|
"Attention" = "ご注意";
|
||||||
"vChewing is upgraded, but please log out or reboot for the new version to be fully functional." = "威注音入力の更新は実装完了しましたが、うまく作動できるために、このパソコンの再起動および再ログインが必要だと恐れ入ります。";
|
"vChewing is upgraded, but please log out or reboot for the new version to be fully functional." = "威注音入力の更新は実装完了しましたが、うまく作動できるために、このパソコンの再起動および再ログインが必要だと恐れ入ります。";
|
||||||
|
"Fatal Error" = "致命錯乱";
|
||||||
|
"Abort" = "中止";
|
||||||
|
"Cannot register input source %@ at %@." = "「%2$@」で入力アプリ「\"%1$@\"」の実装は失敗しました。";
|
||||||
|
"Cannot find input source %@ after registration." = "登録済みですが「%@」は見つけませんでした。";
|
||||||
"Warning" = "お知らせ";
|
"Warning" = "お知らせ";
|
||||||
|
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." = "入力アプリの自動起動はうまく出来なかったかもしれません。ご自分で「システム環境設定→キーボード→入力ソース」で起動してください。";
|
||||||
|
"Continue" = "続行";
|
||||||
|
"i18n:installer.DISCLAIMER_TEXT" = "免責事項:vChewing Project は、OpenVanilla と協力関係や提携関係にあるわけではなく、OpenVanilla が小麦注音プロジェクトに同梱した辞書データについて、vChewing Project は一切責任負い兼ねる。特定な地政学的・観念形態的な内容は、vChewing アプリの世界的な普及に妨害する恐れがあるため、vChewing 公式辞書データに不収録。";
|
||||||
|
"i18n:installer.INSTALLER_APP_TITLE" = "威注音入力 実装用アプリ";
|
||||||
|
"i18n:installer.INSTALLER_APP_TITLE_FULL" = "威注音入力 実装用アプリ";
|
||||||
|
"i18n:installer.ACCEPT_INSTALLATION" = "承認する";
|
||||||
|
"i18n:installer.CANCEL_INSTALLATION" = "取消";
|
||||||
|
"i18n:installer.LICENSE_TITLE" = "MIT商標不許可ライセンス (MIT-NTL License):";
|
||||||
|
"i18n:installer.APP_NAME" = "vChewing for macOS";
|
||||||
|
"i18n:installer.APP_DERIVED_FROM" = "曾て OpenVanilla 小麦注音プロジェクト (MIT-License) から派生。";
|
||||||
|
"i18n:installer.DEV_CREW" = "macOS 版威注音の開発:Shiki Suen, Isaac Xen, Hiraku Wang, など。\n威注音語彙データの維持:Shiki Suen。\nウォーキング算法:Lukhnos Liu (Gramambular 2, MIT-License)。";
|
||||||
|
"i18n:installer.EULA_PROMPT_NOTICE" = "このアプリを実装するために、上記の条約を承認すべきである。";
|
||||||
|
"i18n:installer.STOPPING_THE_OLD_VERSION" = "古いバージョンを強制停止中。1分かかると恐れ入りますが……";
|
||||||
|
|
|
@ -1,30 +1,30 @@
|
||||||
"Abort" = "放弃安装";
|
"vChewing Input Method" = "威注音输入法";
|
||||||
"Attention" = "请注意";
|
"i18n:installer.DO_APP_UPGRADE" = "接受并升级";
|
||||||
"Cancel" = "取消";
|
"Cancel" = "取消";
|
||||||
"Cannot activate the input method." = "无法启用输入法。";
|
"Cannot activate the input method." = "无法启用输入法。";
|
||||||
"Cannot copy the file to the destination." = "无法将输入法拷贝至目的地。";
|
"Cannot copy the file to the destination." = "无法将输入法拷贝至目的地。";
|
||||||
"Cannot find input source %@ after registration." = "在注册完输入法 \"%@\" 之后仍然无法找到该输入法。";
|
|
||||||
"Cannot register input source %@ at %@." = "无法从档案位置 %2$@ 安装输入法 \"%1$@\"。";
|
|
||||||
"Continue" = "继续";
|
|
||||||
"Fatal Error" = "安装错误";
|
|
||||||
"i18n:installer.ACCEPT_INSTALLATION" = "我接受";
|
|
||||||
"i18n:installer.APP_DERIVED_FROM" = "该专案曾由 OpenVanilla 小麦注音专案 (MIT-License) 衍生而来。";
|
|
||||||
"i18n:installer.APP_NAME" = "vChewing for macOS";
|
|
||||||
"i18n:installer.CANCEL_INSTALLATION" = "取消安装";
|
|
||||||
"i18n:installer.DEV_CREW" = "威注音 macOS 程式研发:Shiki Suen, Isaac Xen, Hiraku Wang, 等。\n威注音词库维护:Shiki Suen。\n爬轨算法:Lukhnos Liu (Gramambular 2, MIT-License)。";
|
|
||||||
"i18n:installer.DISCLAIMER_TEXT" = "免责声明:威注音专案对小麦注音官方专案内赠的小麦注音原版词库内容不负任何责任。威注音输入法专用的威注音官方词库不包含任何「会在法理上妨碍威注音在全球传播」的「与地缘政治及政治意识形态有关的」内容。威注音专案与 OpenVanilla 专案之间无合作关系、无隶属关系。";
|
|
||||||
"i18n:installer.DO_APP_UPGRADE" = "接受并升级";
|
|
||||||
"i18n:installer.EULA_PROMPT_NOTICE" = "若要安装该软件,请接受上述条款。";
|
|
||||||
"i18n:installer.INSTALLER_APP_TITLE_FULL" = "威注音输入法安装程式";
|
|
||||||
"i18n:installer.INSTALLER_APP_TITLE" = "威注音安装程式";
|
|
||||||
"i18n:installer.LICENSE_TITLE" = "麻理去商标授权合约 (MIT-NTL License):";
|
|
||||||
"i18n:installer.STOPPING_THE_OLD_VERSION" = "等待旧版完全停用,大约需要一分钟…";
|
|
||||||
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." = "输入法已经安装好,但可能没有完全启用。请从「系统偏好设定」 > 「键盘」 > 「输入方式」分页加入输入法。";
|
|
||||||
"Install Failed" = "安装失败";
|
"Install Failed" = "安装失败";
|
||||||
"Installation Successful" = "安装成功";
|
"Installation Successful" = "安装成功";
|
||||||
"OK" = "确定";
|
"OK" = "确定";
|
||||||
"Stopping the old version. This may take up to one minute…" = "正在试图结束正在运行的旧版输入法,大概需要一分钟…";
|
|
||||||
"vChewing Input Method" = "威注音输入法";
|
|
||||||
"vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account." = "威注音输入法安装成功。\n\n若是在當前使用者帳戶內首次安裝的話,請重新登入。";
|
"vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account." = "威注音输入法安装成功。\n\n若是在當前使用者帳戶內首次安裝的話,請重新登入。";
|
||||||
|
"Stopping the old version. This may take up to one minute…" = "正在试图结束正在运行的旧版输入法,大概需要一分钟…";
|
||||||
|
"Attention" = "请注意";
|
||||||
"vChewing is upgraded, but please log out or reboot for the new version to be fully functional." = "vChewing 安装完成,但建议您登出或重新开机,以便顺利使用新版。";
|
"vChewing is upgraded, but please log out or reboot for the new version to be fully functional." = "vChewing 安装完成,但建议您登出或重新开机,以便顺利使用新版。";
|
||||||
|
"Fatal Error" = "安装错误";
|
||||||
|
"Abort" = "放弃安装";
|
||||||
|
"Cannot register input source %@ at %@." = "无法从档案位置 %2$@ 安装输入法 \"%1$@\"。";
|
||||||
|
"Cannot find input source %@ after registration." = "在注册完输入法 \"%@\" 之后仍然无法找到该输入法。";
|
||||||
"Warning" = "安装不完整";
|
"Warning" = "安装不完整";
|
||||||
|
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." = "输入法已经安装好,但可能没有完全启用。请从「系统偏好设定」 > 「键盘」 > 「输入方式」分页加入输入法。";
|
||||||
|
"Continue" = "继续";
|
||||||
|
"i18n:installer.DISCLAIMER_TEXT" = "免责声明:威注音专案对小麦注音官方专案内赠的小麦注音原版词库内容不负任何责任。威注音输入法专用的威注音官方词库不包含任何「会在法理上妨碍威注音在全球传播」的「与地缘政治及政治意识形态有关的」内容。威注音专案与 OpenVanilla 专案之间无合作关系、无隶属关系。";
|
||||||
|
"i18n:installer.INSTALLER_APP_TITLE" = "威注音安装程式";
|
||||||
|
"i18n:installer.INSTALLER_APP_TITLE_FULL" = "威注音输入法安装程式";
|
||||||
|
"i18n:installer.ACCEPT_INSTALLATION" = "我接受";
|
||||||
|
"i18n:installer.CANCEL_INSTALLATION" = "取消安装";
|
||||||
|
"i18n:installer.LICENSE_TITLE" = "麻理去商标授权合约 (MIT-NTL License):";
|
||||||
|
"i18n:installer.APP_NAME" = "vChewing for macOS";
|
||||||
|
"i18n:installer.APP_DERIVED_FROM" = "该专案曾由 OpenVanilla 小麦注音专案 (MIT-License) 衍生而来。";
|
||||||
|
"i18n:installer.DEV_CREW" = "威注音 macOS 程式研发:Shiki Suen, Isaac Xen, Hiraku Wang, 等。\n威注音词库维护:Shiki Suen。\n爬轨算法:Lukhnos Liu (Gramambular 2, MIT-License)。";
|
||||||
|
"i18n:installer.EULA_PROMPT_NOTICE" = "若要安装该软件,请接受上述条款。";
|
||||||
|
"i18n:installer.STOPPING_THE_OLD_VERSION" = "等待旧版完全停用,大约需要一分钟…";
|
||||||
|
|
|
@ -1,30 +1,30 @@
|
||||||
"Abort" = "放棄安裝";
|
"vChewing Input Method" = "威注音輸入法";
|
||||||
"Attention" = "請注意";
|
"i18n:installer.DO_APP_UPGRADE" = "接受並升級";
|
||||||
"Cancel" = "取消";
|
"Cancel" = "取消";
|
||||||
"Cannot activate the input method." = "無法啟用輸入法。";
|
"Cannot activate the input method." = "無法啟用輸入法。";
|
||||||
"Cannot copy the file to the destination." = "無法將輸入法拷貝至目的地。";
|
"Cannot copy the file to the destination." = "無法將輸入法拷貝至目的地。";
|
||||||
"Cannot find input source %@ after registration." = "在註冊完輸入法 \"%@\" 之後仍然無法找到該輸入法。";
|
|
||||||
"Cannot register input source %@ at %@." = "無法從檔案位置 %2$@ 安裝輸入法 \"%1$@\"。";
|
|
||||||
"Continue" = "繼續";
|
|
||||||
"Fatal Error" = "安裝錯誤";
|
|
||||||
"i18n:installer.ACCEPT_INSTALLATION" = "我接受";
|
|
||||||
"i18n:installer.APP_DERIVED_FROM" = "該專案曾由 OpenVanilla 小麥注音專案 (MIT-License) 衍生而來。";
|
|
||||||
"i18n:installer.APP_NAME" = "vChewing for macOS";
|
|
||||||
"i18n:installer.CANCEL_INSTALLATION" = "取消安裝";
|
|
||||||
"i18n:installer.DEV_CREW" = "威注音 macOS 程式研發:Shiki Suen, Isaac Xen, Hiraku Wang, 等。\n威注音詞庫維護:Shiki Suen。\n爬軌算法:Lukhnos Liu (Gramambular 2, MIT-License)。";
|
|
||||||
"i18n:installer.DISCLAIMER_TEXT" = "免責聲明:威注音專案對小麥注音官方專案內贈的小麥注音原版詞庫內容不負任何責任。威注音輸入法專用的威注音官方詞庫不包含任何「會在法理上妨礙威注音在全球傳播」的「與地緣政治及政治意識形態有關的」內容。威註音專案與 OpenVanilla 專案之間無合作關係、無隸屬關係。";
|
|
||||||
"i18n:installer.DO_APP_UPGRADE" = "接受並升級";
|
|
||||||
"i18n:installer.EULA_PROMPT_NOTICE" = "若要安裝該軟體,請接受上述條款。";
|
|
||||||
"i18n:installer.INSTALLER_APP_TITLE_FULL" = "威注音輸入法安裝程式";
|
|
||||||
"i18n:installer.INSTALLER_APP_TITLE" = "威注音安裝程式";
|
|
||||||
"i18n:installer.LICENSE_TITLE" = "麻理去商標授權合約 (MIT-NTL License):";
|
|
||||||
"i18n:installer.STOPPING_THE_OLD_VERSION" = "等待舊版完全停用,大約需要一分鐘…";
|
|
||||||
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." = "輸入法已經安裝好,但可能沒有完全啟用。請從「系統偏好設定」 > 「鍵盤」 > 「輸入方式」分頁加入輸入法。";
|
|
||||||
"Install Failed" = "安裝失敗";
|
"Install Failed" = "安裝失敗";
|
||||||
"Installation Successful" = "安裝成功";
|
"Installation Successful" = "安裝成功";
|
||||||
"OK" = "確定";
|
"OK" = "確定";
|
||||||
"Stopping the old version. This may take up to one minute…" = "正在試圖結束正在運行的舊版輸入法,大概需要一分鐘…";
|
|
||||||
"vChewing Input Method" = "威注音輸入法";
|
|
||||||
"vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account." = "威注音輸入法安裝成功。\n\n若是在當前使用者帳戶內首次安裝的話,請重新登入。";
|
"vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account." = "威注音輸入法安裝成功。\n\n若是在當前使用者帳戶內首次安裝的話,請重新登入。";
|
||||||
|
"Stopping the old version. This may take up to one minute…" = "正在試圖結束正在運行的舊版輸入法,大概需要一分鐘…";
|
||||||
|
"Attention" = "請注意";
|
||||||
"vChewing is upgraded, but please log out or reboot for the new version to be fully functional." = "vChewing 安裝完成,但建議您登出或重新開機,以便順利使用新版。";
|
"vChewing is upgraded, but please log out or reboot for the new version to be fully functional." = "vChewing 安裝完成,但建議您登出或重新開機,以便順利使用新版。";
|
||||||
|
"Fatal Error" = "安裝錯誤";
|
||||||
|
"Abort" = "放棄安裝";
|
||||||
|
"Cannot register input source %@ at %@." = "無法從檔案位置 %2$@ 安裝輸入法 \"%1$@\"。";
|
||||||
|
"Cannot find input source %@ after registration." = "在註冊完輸入法 \"%@\" 之後仍然無法找到該輸入法。";
|
||||||
"Warning" = "安裝不完整";
|
"Warning" = "安裝不完整";
|
||||||
|
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." = "輸入法已經安裝好,但可能沒有完全啟用。請從「系統偏好設定」 > 「鍵盤」 > 「輸入方式」分頁加入輸入法。";
|
||||||
|
"Continue" = "繼續";
|
||||||
|
"i18n:installer.DISCLAIMER_TEXT" = "免責聲明:威注音專案對小麥注音官方專案內贈的小麥注音原版詞庫內容不負任何責任。威注音輸入法專用的威注音官方詞庫不包含任何「會在法理上妨礙威注音在全球傳播」的「與地緣政治及政治意識形態有關的」內容。威註音專案與 OpenVanilla 專案之間無合作關係、無隸屬關係。";
|
||||||
|
"i18n:installer.INSTALLER_APP_TITLE" = "威注音安裝程式";
|
||||||
|
"i18n:installer.INSTALLER_APP_TITLE_FULL" = "威注音輸入法安裝程式";
|
||||||
|
"i18n:installer.ACCEPT_INSTALLATION" = "我接受";
|
||||||
|
"i18n:installer.CANCEL_INSTALLATION" = "取消安裝";
|
||||||
|
"i18n:installer.LICENSE_TITLE" = "麻理去商標授權合約 (MIT-NTL License):";
|
||||||
|
"i18n:installer.APP_NAME" = "vChewing for macOS";
|
||||||
|
"i18n:installer.APP_DERIVED_FROM" = "該專案曾由 OpenVanilla 小麥注音專案 (MIT-License) 衍生而來。";
|
||||||
|
"i18n:installer.DEV_CREW" = "威注音 macOS 程式研發:Shiki Suen, Isaac Xen, Hiraku Wang, 等。\n威注音詞庫維護:Shiki Suen。\n爬軌算法:Lukhnos Liu (Gramambular 2, MIT-License)。";
|
||||||
|
"i18n:installer.EULA_PROMPT_NOTICE" = "若要安裝該軟體,請接受上述條款。";
|
||||||
|
"i18n:installer.STOPPING_THE_OLD_VERSION" = "等待舊版完全停用,大約需要一分鐘…";
|
||||||
|
|
|
@ -19,4 +19,4 @@ OS_Version=$(sw_vers -productVersion)
|
||||||
##### fi
|
##### fi
|
||||||
|
|
||||||
# Finally, register the input method:
|
# Finally, register the input method:
|
||||||
/Users/"${login_user}"/Library/Input\ Methods/"${TARGET}".app/Contents/MacOS/"${TARGET}" install || true
|
/Users/"${login_user}"/Library/Input\ Methods/"${TARGET}".app/Contents/MacOS/"${TARGET}" install --all || true
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
#!/usr/bin/env swift
|
|
||||||
|
|
||||||
// Copyright (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
|
|
||||||
|
|
||||||
let strDataPath = "./"
|
|
||||||
|
|
||||||
func handleFiles(_ handler: @escaping ((url: URL, fileName: String)) -> Void) {
|
|
||||||
let rawURLs = FileManager.default.enumerator(at: URL(fileURLWithPath: strDataPath), includingPropertiesForKeys: nil)?.compactMap { $0 as? URL }
|
|
||||||
rawURLs?.forEach { url in
|
|
||||||
guard let fileName = url.pathComponents.last, fileName.lowercased() == "localizable.strings" else { return }
|
|
||||||
handler((url, fileName))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleFiles { url, fileName in
|
|
||||||
guard let rawStr = try? String(contentsOf: url, encoding: .utf8) else { return }
|
|
||||||
let locale = Locale(identifier: "zh@collation=stroke")
|
|
||||||
do {
|
|
||||||
try rawStr.components(separatedBy: .newlines).filter { !$0.isEmpty }.sorted {
|
|
||||||
$0.compare($1, locale: locale) == .orderedAscending
|
|
||||||
}.joined(separator: "\n").description.appending("\n").write(to: url, atomically: false, encoding: .utf8)
|
|
||||||
} catch {
|
|
||||||
print("!! Error writing to \(fileName)")
|
|
||||||
}
|
|
||||||
}
|
|
19
Makefile
19
Makefile
|
@ -11,24 +11,6 @@ BUILD_SETTINGS += ARCHS="$(ARCHS)"
|
||||||
BUILD_SETTINGS += ONLY_ACTIVE_ARCH=NO
|
BUILD_SETTINGS += ONLY_ACTIVE_ARCH=NO
|
||||||
endif
|
endif
|
||||||
|
|
||||||
spmDebug:
|
|
||||||
swift build -c debug --package-path ./Packages/vChewing_MainAssembly/
|
|
||||||
|
|
||||||
spmRelease:
|
|
||||||
swift build -c release --package-path ./Packages/vChewing_MainAssembly/
|
|
||||||
|
|
||||||
spmLintFormat:
|
|
||||||
make lint --file=./Packages/Makefile || true
|
|
||||||
make format --file=./Packages/Makefile || true
|
|
||||||
|
|
||||||
spmClean:
|
|
||||||
@for currentDir in $$(ls ./Packages/); do \
|
|
||||||
if [ -d $$a ]; then \
|
|
||||||
echo "processing folder $$currentDir"; \
|
|
||||||
swift package clean --package-path ./Packages/$$currentDir || true; \
|
|
||||||
fi; \
|
|
||||||
done;
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
xcodebuild -project vChewing.xcodeproj -scheme vChewingInstaller -configuration Release $(BUILD_SETTINGS) build
|
xcodebuild -project vChewing.xcodeproj -scheme vChewingInstaller -configuration Release $(BUILD_SETTINGS) build
|
||||||
|
|
||||||
|
@ -60,7 +42,6 @@ install-release: permission-check
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
make clean --file=./Packages/Makefile || true
|
|
||||||
xcodebuild -scheme vChewingInstaller -configuration Debug $(BUILD_SETTINGS) clean
|
xcodebuild -scheme vChewingInstaller -configuration Debug $(BUILD_SETTINGS) clean
|
||||||
xcodebuild -scheme vChewingInstaller -configuration Release $(BUILD_SETTINGS) clean
|
xcodebuild -scheme vChewingInstaller -configuration Release $(BUILD_SETTINGS) clean
|
||||||
make clean --file=./Source/Data/Makefile || true
|
make clean --file=./Source/Data/Makefile || true
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
BasedOnStyle: Microsoft
|
|
|
@ -1,104 +0,0 @@
|
||||||
# SwiftFormat config compliant with Google Swift Guideline
|
|
||||||
# https://google.github.io/swift/#control-flow-statements
|
|
||||||
|
|
||||||
# Specify version used in a project
|
|
||||||
|
|
||||||
--swiftversion 5.5
|
|
||||||
|
|
||||||
# Rules explicitly required by the guideline
|
|
||||||
|
|
||||||
--rules \
|
|
||||||
blankLinesAroundMark, \
|
|
||||||
blankLinesAtEndOfScope, \
|
|
||||||
blankLinesAtStartOfScope, \
|
|
||||||
blankLinesBetweenScopes, \
|
|
||||||
braces, \
|
|
||||||
consecutiveBlankLines, \
|
|
||||||
consecutiveSpaces, \
|
|
||||||
duplicateImports, \
|
|
||||||
elseOnSameLine, \
|
|
||||||
emptyBraces, \
|
|
||||||
enumNamespaces, \
|
|
||||||
extensionAccessControl, \
|
|
||||||
hoistPatternLet, \
|
|
||||||
indent, \
|
|
||||||
leadingDelimiters, \
|
|
||||||
linebreakAtEndOfFile, \
|
|
||||||
markTypes, \
|
|
||||||
organizeDeclarations, \
|
|
||||||
redundantInit, \
|
|
||||||
redundantParens, \
|
|
||||||
redundantPattern, \
|
|
||||||
redundantRawValues, \
|
|
||||||
redundantType, \
|
|
||||||
redundantVoidReturnType, \
|
|
||||||
semicolons, \
|
|
||||||
sortedImports, \
|
|
||||||
sortedSwitchCases, \
|
|
||||||
spaceAroundBraces, \
|
|
||||||
spaceAroundBrackets, \
|
|
||||||
spaceAroundComments, \
|
|
||||||
spaceAroundGenerics, \
|
|
||||||
spaceAroundOperators, \
|
|
||||||
spaceAroundParens, \
|
|
||||||
spaceInsideBraces, \
|
|
||||||
spaceInsideBrackets, \
|
|
||||||
spaceInsideComments, \
|
|
||||||
spaceInsideGenerics, \
|
|
||||||
spaceInsideParens, \
|
|
||||||
todos, \
|
|
||||||
trailingClosures, \
|
|
||||||
trailingCommas, \
|
|
||||||
trailingSpace, \
|
|
||||||
typeSugar, \
|
|
||||||
void, \
|
|
||||||
wrap, \
|
|
||||||
wrapArguments, \
|
|
||||||
wrapAttributes, \
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# Additional rules not mentioned in the guideline, but helping to keep the codebase clean
|
|
||||||
# Quoting the guideline:
|
|
||||||
# Common themes among the rules in this section are:
|
|
||||||
# avoid redundancy, avoid ambiguity, and prefer implicitness over explicitness
|
|
||||||
# unless being explicit improves readability and/or reduces ambiguity.
|
|
||||||
#
|
|
||||||
#
|
|
||||||
andOperator, \
|
|
||||||
isEmpty, \
|
|
||||||
redundantBackticks, \
|
|
||||||
redundantBreak, \
|
|
||||||
redundantExtensionACL, \
|
|
||||||
redundantGet, \
|
|
||||||
redundantLetError, \
|
|
||||||
redundantNilInit, \
|
|
||||||
redundantObjc, \
|
|
||||||
redundantReturn, \
|
|
||||||
redundantSelf, \
|
|
||||||
strongifiedSelf
|
|
||||||
|
|
||||||
|
|
||||||
# Options for basic rules
|
|
||||||
|
|
||||||
--extensionacl on-declarations
|
|
||||||
--funcattributes prev-line
|
|
||||||
--indent 2
|
|
||||||
--maxwidth 100
|
|
||||||
--typeattributes prev-line
|
|
||||||
--varattributes prev-line
|
|
||||||
--voidtype tuple
|
|
||||||
--wraparguments before-first
|
|
||||||
--wrapparameters before-first
|
|
||||||
--wrapcollections before-first
|
|
||||||
--wrapreturntype if-multiline
|
|
||||||
--wrapconditions after-first
|
|
||||||
|
|
||||||
# Option for additional rules
|
|
||||||
|
|
||||||
--self init-only
|
|
||||||
|
|
||||||
# Excluded folders
|
|
||||||
|
|
||||||
--exclude Pods,**/UNTESTED_TODO,vendor,fastlane
|
|
||||||
|
|
||||||
# https://github.com/NoemiRozpara/Google-SwiftFormat-Config
|
|
|
@ -13,13 +13,13 @@ let package = Package(
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(path: "../vChewing_OSFrameworkImpl"),
|
.package(path: "../vChewing_CocoaExtension"),
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.target(
|
.target(
|
||||||
name: "NSAttributedTextView",
|
name: "NSAttributedTextView",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.product(name: "OSFrameworkImpl", package: "vChewing_OSFrameworkImpl"),
|
.product(name: "CocoaExtension", package: "vChewing_CocoaExtension"),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
// Modified by The vChewing Project in order to use it with AppKit.
|
// Modified by The vChewing Project in order to use it with AppKit.
|
||||||
|
|
||||||
import AppKit
|
import AppKit
|
||||||
import OSFrameworkImpl
|
import CocoaExtension
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@available(macOS 10.15, *)
|
@available(macOS 10.15, *)
|
||||||
|
|
|
@ -7,9 +7,9 @@
|
||||||
// requirements defined in MIT License.
|
// requirements defined in MIT License.
|
||||||
|
|
||||||
import AppKit
|
import AppKit
|
||||||
|
import CocoaExtension
|
||||||
import Foundation
|
import Foundation
|
||||||
@testable import NSAttributedTextView
|
@testable import NSAttributedTextView
|
||||||
import OSFrameworkImpl
|
|
||||||
import Shared
|
import Shared
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
+.PHONY: all
|
|
||||||
|
|
||||||
all: debug
|
|
||||||
|
|
||||||
debug:
|
|
||||||
swift build -c debug --package-path ./vChewing_MainAssembly/
|
|
||||||
|
|
||||||
release:
|
|
||||||
swift build -c release --package-path ./vChewing_MainAssembly/
|
|
||||||
|
|
||||||
clean:
|
|
||||||
@for currentDir in $$(ls ./); do \
|
|
||||||
if [ -d $$a ]; then \
|
|
||||||
echo "processing folder $$currentDir"; \
|
|
||||||
swift package clean --package-path ./$$currentDir || true; \
|
|
||||||
fi; \
|
|
||||||
done;
|
|
||||||
|
|
||||||
.PHONY: lint format
|
|
||||||
|
|
||||||
lintFormat: lint format
|
|
||||||
|
|
||||||
format:
|
|
||||||
@swiftformat --swiftversion 5.5 --indent 2 ./
|
|
||||||
|
|
||||||
lint:
|
|
||||||
@git ls-files --exclude-standard | grep -E '\.swift$$' | swiftlint --fix --autocorrect
|
|
||||||
|
|
||||||
.PHONY: permission-check install-debug install-release
|
|
|
@ -1,6 +1,7 @@
|
||||||
// (c) 2019 and onwards Robert Muckle-Jones (Apache 2.0 License).
|
// (c) 2019 and onwards Robert Muckle-Jones (Apache 2.0 License).
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import SwiftExtension
|
||||||
|
|
||||||
public class LineReader {
|
public class LineReader {
|
||||||
let encoding: String.Encoding
|
let encoding: String.Encoding
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
// 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"]
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
|
|
@ -1,284 +0,0 @@
|
||||||
// (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 國通標準並未定義花括弧。
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,109 +0,0 @@
|
||||||
// (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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
// (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, "⠙⠔⠆⠅⠳⠁⠅⠳⠁⠃⠡⠆⠇⠢⠗⠆⠅⠳⠁⠅⠳⠁⠅⠜⠂⠐⠎⠧⠁⠅⠳⠁⠅⠳⠁⠉⠪⠄⠜⠆⠎⠆⠅⠳⠁⠅⠳⠁⠖⠂⠐⠆")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -19,7 +19,6 @@ public class CandidateCellData: Hashable {
|
||||||
public static var unifiedSize: Double = 16
|
public static var unifiedSize: Double = 16
|
||||||
public static var unifiedCharDimension: Double { ceil(unifiedSize * 1.0125 + 7) }
|
public static var unifiedCharDimension: Double { ceil(unifiedSize * 1.0125 + 7) }
|
||||||
public static var unifiedTextHeight: Double { ceil(unifiedSize * 19 / 16) }
|
public static var unifiedTextHeight: Double { ceil(unifiedSize * 19 / 16) }
|
||||||
static var internalPrefs = PrefMgr()
|
|
||||||
public var selectionKey: String
|
public var selectionKey: String
|
||||||
public let displayedText: String
|
public let displayedText: String
|
||||||
public private(set) var textDimension: NSSize
|
public private(set) var textDimension: NSSize
|
||||||
|
@ -82,8 +81,7 @@ public class CandidateCellData: Hashable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func cellLength(isMatrix: Bool = true) -> Double {
|
public func cellLength(isMatrix: Bool = true) -> Double {
|
||||||
let factor: CGFloat = (Self.internalPrefs.minCellWidthForHorizontalMatrix == 0) ? 1.5 : 2
|
let minLength = ceil(Self.unifiedCharDimension * 2 + size * 1.25)
|
||||||
let minLength = ceil(Self.unifiedCharDimension * factor + size * 1.25)
|
|
||||||
if displayedText.count <= 2, isMatrix { return minLength }
|
if displayedText.count <= 2, isMatrix { return minLength }
|
||||||
return textDimension.width
|
return textDimension.width
|
||||||
}
|
}
|
||||||
|
@ -202,14 +200,14 @@ public class CandidateCellData: Hashable {
|
||||||
return attrStrCandidate
|
return attrStrCandidate
|
||||||
}
|
}
|
||||||
|
|
||||||
public func charDescriptions(shortened: Bool = false) -> [String] {
|
public var charDescriptions: [String] {
|
||||||
var result = displayedText
|
var result = displayedText
|
||||||
if displayedText.contains("("), displayedText.count > 2 {
|
if displayedText.contains("("), displayedText.count > 2 {
|
||||||
result = displayedText.replacingOccurrences(of: "(", with: "").replacingOccurrences(of: ")", with: "")
|
result = displayedText.replacingOccurrences(of: "(", with: "").replacingOccurrences(of: ")", with: "")
|
||||||
}
|
}
|
||||||
return result.flatMap(\.unicodeScalars).compactMap {
|
return result.flatMap(\.unicodeScalars).compactMap {
|
||||||
let theName: String = $0.properties.name ?? ""
|
let theName: String = $0.properties.name ?? ""
|
||||||
return shortened ? String(format: "U+%02X", $0.value) : String(format: "U+%02X %@", $0.value, theName)
|
return String(format: "U+%02X %@", $0.value, theName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -301,12 +301,10 @@ extension CandidatePool {
|
||||||
.font: Self.blankCell.phraseFont(size: reverseLookupTextSize),
|
.font: Self.blankCell.phraseFont(size: reverseLookupTextSize),
|
||||||
]
|
]
|
||||||
let result = NSMutableAttributedString(string: "", attributes: attrReverseLookupSpacer)
|
let result = NSMutableAttributedString(string: "", attributes: attrReverseLookupSpacer)
|
||||||
var addedCounter = 0
|
|
||||||
for neta in reverseLookupResult {
|
for neta in reverseLookupResult {
|
||||||
result.append(NSAttributedString(string: " ", attributes: attrReverseLookupSpacer))
|
result.append(NSAttributedString(string: " ", attributes: attrReverseLookupSpacer))
|
||||||
result.append(NSAttributedString(string: " \(neta) ", attributes: attrReverseLookup))
|
result.append(NSAttributedString(string: " \(neta) ", attributes: attrReverseLookup))
|
||||||
addedCounter += 1
|
if maxLinesPerPage == 1 { break }
|
||||||
if maxLinesPerPage == 1, addedCounter == 2 { break }
|
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,24 +17,26 @@ open class CtlCandidate: NSWindowController, CtlCandidateProtocol {
|
||||||
open var reverseLookupResult: [String] = []
|
open var reverseLookupResult: [String] = []
|
||||||
|
|
||||||
open func highlightedColor() -> NSColor {
|
open func highlightedColor() -> NSColor {
|
||||||
var result = NSColor.clear
|
var result = NSColor.controlAccentColor
|
||||||
if #available(macOS 10.14, *) {
|
var colorBlendAmount: Double = NSApplication.isDarkMode ? 0.3 : 0.0
|
||||||
result = .controlAccentColor
|
if #available(macOS 10.14, *), !NSApplication.isDarkMode, locale == "zh-Hant" {
|
||||||
} else {
|
colorBlendAmount = 0.15
|
||||||
result = .alternateSelectedControlTextColor
|
|
||||||
}
|
}
|
||||||
let colorBlendAmount = 0.3
|
|
||||||
// 設定當前高亮候選字的背景顏色。
|
// 設定當前高亮候選字的背景顏色。
|
||||||
switch locale {
|
switch locale {
|
||||||
case "zh-Hans":
|
case "zh-Hans":
|
||||||
result = NSColor.red
|
result = NSColor.systemRed
|
||||||
case "zh-Hant":
|
case "zh-Hant":
|
||||||
result = NSColor.blue
|
result = NSColor.systemBlue
|
||||||
case "ja":
|
case "ja":
|
||||||
result = NSColor.brown
|
result = NSColor.systemBrown
|
||||||
default: break
|
default: break
|
||||||
}
|
}
|
||||||
let blendingAgainstTarget: NSColor = NSApplication.isDarkMode ? NSColor.black : NSColor.white
|
var blendingAgainstTarget: NSColor = NSApplication.isDarkMode ? NSColor.black : NSColor.white
|
||||||
|
if #unavailable(macOS 10.14) {
|
||||||
|
colorBlendAmount = 0.3
|
||||||
|
blendingAgainstTarget = NSColor.white
|
||||||
|
}
|
||||||
return result.blended(withFraction: colorBlendAmount, of: blendingAgainstTarget)!
|
return result.blended(withFraction: colorBlendAmount, of: blendingAgainstTarget)!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
// requirements defined in MIT License.
|
// requirements defined in MIT License.
|
||||||
|
|
||||||
import AppKit
|
import AppKit
|
||||||
import OSFrameworkImpl
|
import CocoaExtension
|
||||||
import Shared
|
import Shared
|
||||||
|
|
||||||
private extension NSUserInterfaceLayoutOrientation {
|
private extension NSUserInterfaceLayoutOrientation {
|
||||||
|
@ -108,34 +108,16 @@ public class CtlCandidateTDK: CtlCandidate, NSWindowDelegate {
|
||||||
|
|
||||||
override open func updateDisplay() {
|
override open func updateDisplay() {
|
||||||
guard let window = window else { return }
|
guard let window = window else { return }
|
||||||
|
if let currentCandidateText = Self.thePool.currentSelectedCandidateText {
|
||||||
|
reverseLookupResult = delegate?.reverseLookup(for: currentCandidateText) ?? []
|
||||||
|
Self.thePool.reverseLookupResult = reverseLookupResult
|
||||||
|
Self.thePool.tooltip = delegate?.candidateToolTip(shortened: !Self.thePool.isMatrix) ?? ""
|
||||||
|
}
|
||||||
|
delegate?.candidatePairHighlightChanged(at: highlightedIndex)
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.updateNSWindowModern(window)
|
self.updateNSWindowModern(window)
|
||||||
}
|
}
|
||||||
// 先擦除之前的反查结果。
|
|
||||||
reverseLookupResult = []
|
|
||||||
// 再更新新的反查结果。
|
|
||||||
if let currentCandidate = Self.thePool.currentCandidate {
|
|
||||||
let displayedText = currentCandidate.displayedText
|
|
||||||
var lookupResult: [String?] = delegate?.reverseLookup(for: displayedText) ?? []
|
|
||||||
if displayedText.count == 1, delegate?.showCodePointForCurrentCandidate ?? false {
|
|
||||||
if lookupResult.isEmpty {
|
|
||||||
lookupResult.append(currentCandidate.charDescriptions(shortened: !Self.thePool.isMatrix).first)
|
|
||||||
} else {
|
|
||||||
lookupResult.insert(currentCandidate.charDescriptions(shortened: true).first, at: lookupResult.startIndex)
|
|
||||||
}
|
|
||||||
reverseLookupResult = lookupResult.compactMap { $0 }
|
|
||||||
} else {
|
|
||||||
reverseLookupResult = lookupResult.compactMap { $0 }
|
|
||||||
// 如果不提供 UNICODE 碼位資料顯示的話,則在非多行多列模式下僅顯示一筆反查資料。
|
|
||||||
if !Self.thePool.isMatrix {
|
|
||||||
reverseLookupResult = [reverseLookupResult.first].compactMap { $0 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self.thePool.reverseLookupResult = reverseLookupResult
|
|
||||||
Self.thePool.tooltip = delegate?.candidateToolTip(shortened: !Self.thePool.isMatrix) ?? ""
|
|
||||||
delegate?.candidatePairHighlightChanged(at: highlightedIndex)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateNSWindowModern(_ window: NSWindow) {
|
func updateNSWindowModern(_ window: NSWindow) {
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
// requirements defined in MIT License.
|
// requirements defined in MIT License.
|
||||||
|
|
||||||
import AppKit
|
import AppKit
|
||||||
import OSFrameworkImpl
|
|
||||||
import Shared
|
import Shared
|
||||||
|
|
||||||
/// 田所選字窗的 AppKit 简单版本,繪製效率不受 SwiftUI 的限制。
|
/// 田所選字窗的 AppKit 简单版本,繪製效率不受 SwiftUI 的限制。
|
||||||
|
@ -152,17 +151,30 @@ public extension VwrCandidateTDKAppKit {
|
||||||
private extension VwrCandidateTDKAppKit {
|
private extension VwrCandidateTDKAppKit {
|
||||||
private func prepareMenu() {
|
private func prepareMenu() {
|
||||||
let newMenu = NSMenu()
|
let newMenu = NSMenu()
|
||||||
newMenu.appendItems(self) {
|
let boostMenuItem = NSMenuItem(
|
||||||
NSMenu.Item(
|
title: "↑ \(clickedCell.displayedText)",
|
||||||
verbatim: "↑ \(clickedCell.displayedText)"
|
action: #selector(menuActionOfBoosting(_:)),
|
||||||
)?.act(#selector(menuActionOfBoosting(_:)))
|
keyEquivalent: ""
|
||||||
NSMenu.Item(
|
)
|
||||||
verbatim: "↓ \(clickedCell.displayedText)"
|
boostMenuItem.target = self
|
||||||
)?.act(#selector(menuActionOfNerfing(_:)))
|
newMenu.addItem(boostMenuItem)
|
||||||
NSMenu.Item(
|
|
||||||
verbatim: "✖︎ \(clickedCell.displayedText)"
|
let nerfMenuItem = NSMenuItem(
|
||||||
)?.act(#selector(menuActionOfFiltering(_:)))
|
title: "↓ \(clickedCell.displayedText)",
|
||||||
.nulled(!thePool.isFilterable(target: clickedCell.index))
|
action: #selector(menuActionOfNerfing(_:)),
|
||||||
|
keyEquivalent: ""
|
||||||
|
)
|
||||||
|
nerfMenuItem.target = self
|
||||||
|
newMenu.addItem(nerfMenuItem)
|
||||||
|
|
||||||
|
if thePool.isFilterable(target: clickedCell.index) {
|
||||||
|
let filterMenuItem = NSMenuItem(
|
||||||
|
title: "✖︎ \(clickedCell.displayedText)",
|
||||||
|
action: #selector(menuActionOfFiltering(_:)),
|
||||||
|
keyEquivalent: ""
|
||||||
|
)
|
||||||
|
filterMenuItem.target = self
|
||||||
|
newMenu.addItem(filterMenuItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
theMenu = newMenu
|
theMenu = newMenu
|
||||||
|
|
|
@ -2,14 +2,14 @@
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "OSFrameworkImpl",
|
name: "CocoaExtension",
|
||||||
platforms: [
|
platforms: [
|
||||||
.macOS(.v11),
|
.macOS(.v11),
|
||||||
],
|
],
|
||||||
products: [
|
products: [
|
||||||
.library(
|
.library(
|
||||||
name: "OSFrameworkImpl",
|
name: "CocoaExtension",
|
||||||
targets: ["OSFrameworkImpl"]
|
targets: ["CocoaExtension"]
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
@ -17,7 +17,7 @@ let package = Package(
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.target(
|
.target(
|
||||||
name: "OSFrameworkImpl",
|
name: "CocoaExtension",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.product(name: "SwiftExtension", package: "vChewing_SwiftExtension"),
|
.product(name: "SwiftExtension", package: "vChewing_SwiftExtension"),
|
||||||
]
|
]
|
|
@ -1,4 +1,4 @@
|
||||||
# OSFrameworkImpl
|
# CocoaExtension
|
||||||
|
|
||||||
威注音輸入法針對 Cocoa 的一些功能擴充,使程式維護體驗更佳。
|
威注音輸入法針對 Cocoa 的一些功能擴充,使程式維護體驗更佳。
|
||||||
|
|
|
@ -71,18 +71,6 @@ public extension NSAttributedString {
|
||||||
|
|
||||||
public extension NSString {
|
public extension NSString {
|
||||||
var localized: String { NSLocalizedString(description, comment: "") }
|
var localized: String { NSLocalizedString(description, comment: "") }
|
||||||
|
|
||||||
@objc func getCharDescriptions(_: Any? = nil) -> [String] {
|
|
||||||
(self as String).charDescriptions
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func getCodePoints(_: Any? = nil) -> [String] {
|
|
||||||
(self as String).codePoints
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func getDescriptionAsCodePoints(_: Any? = nil) -> [String] {
|
|
||||||
(self as String).describedAsCodePoints
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - NSRange Extension
|
// MARK: - NSRange Extension
|
||||||
|
@ -140,14 +128,15 @@ public extension NSApplication {
|
||||||
// MARK: - System Dark Mode Status Detector.
|
// MARK: - System Dark Mode Status Detector.
|
||||||
|
|
||||||
static var isDarkMode: Bool {
|
static var isDarkMode: Bool {
|
||||||
// "NSApp" can be nil during SPM unit tests.
|
if #unavailable(macOS 10.14) { return false }
|
||||||
// Therefore, the method dedicated for macOS 10.15 and later is not considered stable anymore.
|
if #available(macOS 10.15, *) {
|
||||||
// Fortunately, the method for macOS 10.14 works well on later macOS releases.
|
let appearanceDescription = NSApp.effectiveAppearance.debugDescription
|
||||||
if #available(macOS 10.14, *), let strAIS = UserDefaults.current.string(forKey: "AppleInterfaceStyle") {
|
.lowercased()
|
||||||
return strAIS.lowercased().contains("dark")
|
return appearanceDescription.contains("dark")
|
||||||
} else {
|
} else if let appleInterfaceStyle = UserDefaults.current.string(forKey: "AppleInterfaceStyle") {
|
||||||
return false
|
return appleInterfaceStyle.lowercased().contains("dark")
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Tell whether this IME is running with Root privileges.
|
// MARK: - Tell whether this IME is running with Root privileges.
|
||||||
|
@ -210,6 +199,19 @@ public extension NSApplication {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - String.applyingTransform
|
||||||
|
|
||||||
|
public extension String {
|
||||||
|
func applyingTransformFW2HW(reverse: Bool) -> String {
|
||||||
|
if #available(macOS 10.11, *) {
|
||||||
|
return applyingTransform(.fullwidthToHalfwidth, reverse: reverse) ?? self
|
||||||
|
}
|
||||||
|
let theString = NSMutableString(string: self)
|
||||||
|
CFStringTransform(theString, nil, kCFStringTransformFullwidthHalfwidth, reverse)
|
||||||
|
return theString as String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Check whether current date is the given date.
|
// MARK: - Check whether current date is the given date.
|
||||||
|
|
||||||
public extension Date {
|
public extension Date {
|
||||||
|
@ -325,13 +327,3 @@ public extension NSApplication {
|
||||||
UserDefaults.standard.object(forKey: "AppleAccentColor") != nil
|
UserDefaults.standard.object(forKey: "AppleAccentColor") != nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Pasteboard Type Extension.
|
|
||||||
|
|
||||||
public extension NSPasteboard.PasteboardType {
|
|
||||||
static let kUTTypeFileURL = Self(rawValue: "public.file-url") // import UniformTypeIdentifiers
|
|
||||||
static let kUTTypeData = Self(rawValue: "public.data") // import UniformTypeIdentifiers
|
|
||||||
static let kUTTypeAppBundle = Self(rawValue: "com.apple.application-bundle") // import UniformTypeIdentifiers
|
|
||||||
static let kUTTypeUTF8PlainText = Self(rawValue: "public.utf8-plain-text")
|
|
||||||
static let kNSFilenamesPboardType = Self(rawValue: "NSFilenamesPboardType")
|
|
||||||
}
|
|
|
@ -48,19 +48,12 @@ public extension NSWindowController {
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension NSWindow {
|
public extension NSWindow {
|
||||||
@discardableResult func callAlert(title: String, text: String? = nil) -> NSApplication.ModalResponse {
|
|
||||||
(self as NSWindow?).callAlert(title: title, text: text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public extension NSWindow? {
|
|
||||||
@discardableResult func callAlert(title: String, text: String? = nil) -> NSApplication.ModalResponse {
|
@discardableResult func callAlert(title: String, text: String? = nil) -> NSApplication.ModalResponse {
|
||||||
let alert = NSAlert()
|
let alert = NSAlert()
|
||||||
alert.messageText = title
|
alert.messageText = title
|
||||||
if let text = text { alert.informativeText = text }
|
if let text = text { alert.informativeText = text }
|
||||||
alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
|
alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
|
||||||
var result: NSApplication.ModalResponse = .alertFirstButtonReturn
|
var result: NSApplication.ModalResponse = .alertFirstButtonReturn
|
||||||
guard let self = self else { return alert.runModal() }
|
|
||||||
alert.beginSheetModal(for: self) { theResponce in
|
alert.beginSheetModal(for: self) { theResponce in
|
||||||
result = theResponce
|
result = theResponce
|
||||||
}
|
}
|
|
@ -20,19 +20,15 @@ public class SecureEventInputSputnik {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func getIORegListResults() -> String? {
|
public static func getIORegListResults() -> String? {
|
||||||
// Don't generate results under any of the following situations:
|
|
||||||
// - Hibernation / LoggedOut / SwitchedOut / ScreenSaver situations.
|
|
||||||
guard NSWorkspace.activationFlags.isEmpty else { return nil }
|
|
||||||
var resultDictionaryCF: Unmanaged<CFMutableDictionary>?
|
var resultDictionaryCF: Unmanaged<CFMutableDictionary>?
|
||||||
defer { resultDictionaryCF = nil }
|
|
||||||
/// Regarding the parameter in IORegistryGetRootEntry:
|
/// Regarding the parameter in IORegistryGetRootEntry:
|
||||||
/// Both kIOMasterPortDefault and kIOMainPortDefault are 0.
|
/// Both kIOMasterPortDefault and kIOMainPortDefault are 0.
|
||||||
/// The latter one is similar to what `git` had done: changing "Master" to "Main".
|
/// The latter one is similar to what `git` had done: changing "Master" to "Main".
|
||||||
let statusSucceeded = IORegistryEntryCreateCFProperties(
|
let statusSucceeded = IORegistryEntryCreateCFProperties(
|
||||||
IORegistryGetRootEntry(0), &resultDictionaryCF, kCFAllocatorDefault, IOOptionBits(0)
|
IORegistryGetRootEntry(0), &resultDictionaryCF, kCFAllocatorDefault, IOOptionBits(0)
|
||||||
)
|
)
|
||||||
let dict: CFMutableDictionary? = resultDictionaryCF?.takeRetainedValue()
|
|
||||||
guard statusSucceeded == KERN_SUCCESS else { return nil }
|
guard statusSucceeded == KERN_SUCCESS else { return nil }
|
||||||
|
let dict = resultDictionaryCF?.takeRetainedValue()
|
||||||
guard let dict: [CFString: Any] = dict as? [CFString: Any] else { return nil }
|
guard let dict: [CFString: Any] = dict as? [CFString: Any] else { return nil }
|
||||||
return (dict.description)
|
return (dict.description)
|
||||||
}
|
}
|
||||||
|
@ -79,7 +75,7 @@ public extension NSWorkspace {
|
||||||
|
|
||||||
public static let hibernating = ActivationFlags(rawValue: 1 << 0)
|
public static let hibernating = ActivationFlags(rawValue: 1 << 0)
|
||||||
public static let desktopLocked = ActivationFlags(rawValue: 1 << 1)
|
public static let desktopLocked = ActivationFlags(rawValue: 1 << 1)
|
||||||
public static let sessionSwitchedOut = ActivationFlags(rawValue: 1 << 2)
|
public static let sesssionSwitchedOut = ActivationFlags(rawValue: 1 << 2)
|
||||||
public static let screenSaverRunning = ActivationFlags(rawValue: 1 << 3)
|
public static let screenSaverRunning = ActivationFlags(rawValue: 1 << 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,11 +123,11 @@ extension SecureEventInputSputnik {
|
||||||
.store(in: &Self.combinePool)
|
.store(in: &Self.combinePool)
|
||||||
NSWorkspace.shared.notificationCenter
|
NSWorkspace.shared.notificationCenter
|
||||||
.publisher(for: NSWorkspace.sessionDidResignActiveNotification)
|
.publisher(for: NSWorkspace.sessionDidResignActiveNotification)
|
||||||
.sink { _ in NSWorkspace.activationFlags.insert(.sessionSwitchedOut) }
|
.sink { _ in NSWorkspace.activationFlags.insert(.sesssionSwitchedOut) }
|
||||||
.store(in: &Self.combinePool)
|
.store(in: &Self.combinePool)
|
||||||
NSWorkspace.shared.notificationCenter
|
NSWorkspace.shared.notificationCenter
|
||||||
.publisher(for: NSWorkspace.sessionDidBecomeActiveNotification)
|
.publisher(for: NSWorkspace.sessionDidBecomeActiveNotification)
|
||||||
.sink { _ in NSWorkspace.activationFlags.remove(.sessionSwitchedOut) }
|
.sink { _ in NSWorkspace.activationFlags.remove(.sesssionSwitchedOut) }
|
||||||
.store(in: &Self.combinePool)
|
.store(in: &Self.combinePool)
|
||||||
} else {
|
} else {
|
||||||
Self.combinePoolCocoa.append(
|
Self.combinePoolCocoa.append(
|
||||||
|
@ -173,13 +169,13 @@ extension SecureEventInputSputnik {
|
||||||
Self.combinePoolCocoa.append(
|
Self.combinePoolCocoa.append(
|
||||||
NSWorkspace.shared.notificationCenter
|
NSWorkspace.shared.notificationCenter
|
||||||
.addObserver(forName: NSWorkspace.sessionDidResignActiveNotification, object: nil, queue: .main) { _ in
|
.addObserver(forName: NSWorkspace.sessionDidResignActiveNotification, object: nil, queue: .main) { _ in
|
||||||
NSWorkspace.activationFlags.insert(.sessionSwitchedOut)
|
NSWorkspace.activationFlags.insert(.sesssionSwitchedOut)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
Self.combinePoolCocoa.append(
|
Self.combinePoolCocoa.append(
|
||||||
NSWorkspace.shared.notificationCenter
|
NSWorkspace.shared.notificationCenter
|
||||||
.addObserver(forName: NSWorkspace.sessionDidBecomeActiveNotification, object: nil, queue: .main) { _ in
|
.addObserver(forName: NSWorkspace.sessionDidBecomeActiveNotification, object: nil, queue: .main) { _ in
|
||||||
NSWorkspace.activationFlags.remove(.sessionSwitchedOut)
|
NSWorkspace.activationFlags.remove(.sesssionSwitchedOut)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -100,7 +100,9 @@ public class HotenkaChineseConverter {
|
||||||
dictFiles = .init()
|
dictFiles = .init()
|
||||||
do {
|
do {
|
||||||
let rawData = try Data(contentsOf: URL(fileURLWithPath: jsonDir))
|
let rawData = try Data(contentsOf: URL(fileURLWithPath: jsonDir))
|
||||||
let rawJSON = try JSONDecoder().decode([String: [String: String]].self, from: rawData)
|
guard let rawJSON: [String: [String: String]] = try JSONSerialization.jsonObject(with: rawData) as? [String: [String: String]] else {
|
||||||
|
throw NSError()
|
||||||
|
}
|
||||||
dict = rawJSON
|
dict = rawJSON
|
||||||
} catch {
|
} catch {
|
||||||
NSLog("// Exception happened when reading dict json at: \(jsonDir).")
|
NSLog("// Exception happened when reading dict json at: \(jsonDir).")
|
||||||
|
|
|
@ -40,10 +40,7 @@ extension HotenkaTests {
|
||||||
let testInstance: HotenkaChineseConverter = .init(dictDir: testDataPath)
|
let testInstance: HotenkaChineseConverter = .init(dictDir: testDataPath)
|
||||||
NSLog("// Loading complete. Generating json dict file.")
|
NSLog("// Loading complete. Generating json dict file.")
|
||||||
do {
|
do {
|
||||||
let urlOutput = URL(fileURLWithPath: testDataPath + "convdict.json")
|
try JSONSerialization.data(withJSONObject: testInstance.dict, options: .sortedKeys).write(to: URL(fileURLWithPath: testDataPath + "convdict.json"))
|
||||||
let encoder = JSONEncoder()
|
|
||||||
encoder.outputFormatting = .sortedKeys
|
|
||||||
try encoder.encode(testInstance.dict).write(to: urlOutput, options: .atomic)
|
|
||||||
} catch {
|
} catch {
|
||||||
NSLog("// Error on writing strings to file: \(error)")
|
NSLog("// Error on writing strings to file: \(error)")
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,13 +15,23 @@ public enum IMKHelper {
|
||||||
/// 威注音有專門統計過,實際上會有差異的英數鍵盤佈局只有這幾種。
|
/// 威注音有專門統計過,實際上會有差異的英數鍵盤佈局只有這幾種。
|
||||||
/// 精簡成這種清單的話,不但節省 SwiftUI 的繪製壓力,也方便使用者做選擇。
|
/// 精簡成這種清單的話,不但節省 SwiftUI 的繪製壓力,也方便使用者做選擇。
|
||||||
public static let arrWhitelistedKeyLayoutsASCII: [String] = {
|
public static let arrWhitelistedKeyLayoutsASCII: [String] = {
|
||||||
var results = LatinKeyboardMappings.allCases
|
var result = [
|
||||||
if #available(macOS 10.13, *) {
|
"com.apple.keylayout.ABC",
|
||||||
results = results.filter {
|
"com.apple.keylayout.ABC-AZERTY",
|
||||||
![.qwertyUS, .qwertzGerman, .azertyFrench].contains($0)
|
"com.apple.keylayout.ABC-QWERTZ",
|
||||||
|
"com.apple.keylayout.British",
|
||||||
|
"com.apple.keylayout.Colemak",
|
||||||
|
"com.apple.keylayout.Dvorak",
|
||||||
|
"com.apple.keylayout.Dvorak-Left",
|
||||||
|
"com.apple.keylayout.DVORAK-QWERTYCMD",
|
||||||
|
"com.apple.keylayout.Dvorak-Right",
|
||||||
|
]
|
||||||
|
if #unavailable(macOS 10.13) {
|
||||||
|
result.append("com.apple.keylayout.US")
|
||||||
|
result.append("com.apple.keylayout.German")
|
||||||
|
result.append("com.apple.keylayout.French")
|
||||||
}
|
}
|
||||||
}
|
return result
|
||||||
return results.map(\.rawValue)
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
public static let arrDynamicBasicKeyLayouts: [String] = [
|
public static let arrDynamicBasicKeyLayouts: [String] = [
|
||||||
|
@ -39,31 +49,33 @@ public enum IMKHelper {
|
||||||
"org.unknown.keylayout.vChewingMiTAC",
|
"org.unknown.keylayout.vChewingMiTAC",
|
||||||
]
|
]
|
||||||
|
|
||||||
public static var allowedAlphanumericalTISInputSources: [TISInputSource.KeyboardLayout] {
|
public static var allowedAlphanumericalTISInputSources: [TISInputSource] {
|
||||||
let allTISKeyboardLayouts = TISInputSource.getAllTISInputKeyboardLayoutMap()
|
arrWhitelistedKeyLayoutsASCII.compactMap { TISInputSource.generate(from: $0) }
|
||||||
return arrWhitelistedKeyLayoutsASCII.compactMap { allTISKeyboardLayouts[$0] }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var allowedBasicLayoutsAsTISInputSources: [TISInputSource.KeyboardLayout?] {
|
public static var allowedBasicLayoutsAsTISInputSources: [TISInputSource?] {
|
||||||
let allTISKeyboardLayouts = TISInputSource.getAllTISInputKeyboardLayoutMap()
|
// 為了保證清單順序,先弄兩個容器。
|
||||||
// 為了保證清單順序,先弄幾個容器。
|
var containerA: [TISInputSource?] = []
|
||||||
var containerA: [TISInputSource.KeyboardLayout?] = []
|
var containerB: [TISInputSource?] = []
|
||||||
var containerB: [TISInputSource.KeyboardLayout?] = []
|
var containerC: [TISInputSource?] = []
|
||||||
var containerC: [TISInputSource.KeyboardLayout] = []
|
|
||||||
|
|
||||||
let filterSet = Array(Set(arrWhitelistedKeyLayoutsASCII).subtracting(Set(arrDynamicBasicKeyLayouts)))
|
let rawDictionary = TISInputSource.rawTISInputSources(onlyASCII: false)
|
||||||
let matchedGroupBasic = (arrWhitelistedKeyLayoutsASCII + arrDynamicBasicKeyLayouts).compactMap {
|
|
||||||
allTISKeyboardLayouts[$0]
|
Self.arrWhitelistedKeyLayoutsASCII.forEach {
|
||||||
}
|
if let neta = rawDictionary[$0], !arrDynamicBasicKeyLayouts.contains(neta.identifier) {
|
||||||
matchedGroupBasic.forEach { neta in
|
|
||||||
if filterSet.contains(neta.id) {
|
|
||||||
containerC.append(neta)
|
containerC.append(neta)
|
||||||
} else if neta.id.hasPrefix("com.apple") {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Self.arrDynamicBasicKeyLayouts.forEach {
|
||||||
|
if let neta = rawDictionary[$0] {
|
||||||
|
if neta.identifier.contains("com.apple") {
|
||||||
containerA.append(neta)
|
containerA.append(neta)
|
||||||
} else {
|
} else {
|
||||||
containerB.append(neta)
|
containerB.append(neta)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 這裡的 nil 是用來讓選單插入分隔符用的。
|
// 這裡的 nil 是用來讓選單插入分隔符用的。
|
||||||
if !containerA.isEmpty { containerA.append(nil) }
|
if !containerA.isEmpty { containerA.append(nil) }
|
||||||
|
|
|
@ -8,27 +8,25 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public enum LatinKeyboardMappings: String, CaseIterable {
|
public enum LatinKeyboardMappings: String {
|
||||||
case qwerty = "com.apple.keylayout.ABC"
|
case qwerty = "com.apple.keylayout.ABC"
|
||||||
case qwertyBritish = "com.apple.keylayout.British"
|
case qwertyUS = "com.apple.keylayout.US"
|
||||||
case qwertyUS = "com.apple.keylayout.US" // 10.9 - 10.12
|
|
||||||
case azerty = "com.apple.keylayout.ABC-AZERTY"
|
case azerty = "com.apple.keylayout.ABC-AZERTY"
|
||||||
|
case azertyFrench = "com.apple.keylayout.French"
|
||||||
case qwertz = "com.apple.keylayout.ABC-QWERTZ"
|
case qwertz = "com.apple.keylayout.ABC-QWERTZ"
|
||||||
case azertyFrench = "com.apple.keylayout.French" // 10.9 - 10.12
|
case qwertyGerman = "com.apple.keylayout.German"
|
||||||
case qwertzGerman = "com.apple.keylayout.German" // 10.9 - 10.12
|
|
||||||
case colemak = "com.apple.keylayout.Colemak"
|
case colemak = "com.apple.keylayout.Colemak"
|
||||||
case dvorak = "com.apple.keylayout.Dvorak"
|
case dvorak = "com.apple.keylayout.Dvorak"
|
||||||
case dvorakQwertyCMD = "com.apple.keylayout.DVORAK-QWERTYCMD"
|
|
||||||
case dvorakLeft = "com.apple.keylayout.Dvorak-Left"
|
case dvorakLeft = "com.apple.keylayout.Dvorak-Left"
|
||||||
case dvorakRight = "com.apple.keylayout.Dvorak-Right"
|
case dvorakRight = "com.apple.keylayout.Dvorak-Right"
|
||||||
|
|
||||||
public var mapTable: [UInt16: (String, String)] {
|
public var mapTable: [UInt16: (String, String)] {
|
||||||
switch self {
|
switch self {
|
||||||
case .qwerty, .qwertyUS, .qwertyBritish: return Self.dictQwerty
|
case .qwerty, .qwertyUS: return Self.dictQwerty
|
||||||
case .azerty, .azertyFrench: return Self.dictAzerty
|
case .azerty, .azertyFrench: return Self.dictAzerty
|
||||||
case .qwertz, .qwertzGerman: return Self.dictQwertz
|
case .qwertz, .qwertyGerman: return Self.dictQwertz
|
||||||
case .colemak: return Self.dictColemak
|
case .colemak: return Self.dictColemak
|
||||||
case .dvorak, .dvorakQwertyCMD: return Self.dictDvorak
|
case .dvorak: return Self.dictDvorak
|
||||||
case .dvorakLeft: return Self.dictDvorakLeft
|
case .dvorakLeft: return Self.dictDvorakLeft
|
||||||
case .dvorakRight: return Self.dictDvorakRight
|
case .dvorakRight: return Self.dictDvorakRight
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,13 +12,8 @@ import InputMethodKit
|
||||||
// MARK: - TISInputSource Extension by The vChewing Project (MIT-NTL License).
|
// MARK: - TISInputSource Extension by The vChewing Project (MIT-NTL License).
|
||||||
|
|
||||||
public extension TISInputSource {
|
public extension TISInputSource {
|
||||||
struct KeyboardLayout: Identifiable {
|
|
||||||
public var id: String
|
|
||||||
public var titleLocalized: String
|
|
||||||
}
|
|
||||||
|
|
||||||
static var allRegisteredInstancesOfThisInputMethod: [TISInputSource] {
|
static var allRegisteredInstancesOfThisInputMethod: [TISInputSource] {
|
||||||
TISInputSource.match(modeIDs: TISInputSource.modes)
|
TISInputSource.modes.compactMap { TISInputSource.generate(from: $0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
static var modes: [String] {
|
static var modes: [String] {
|
||||||
|
@ -27,7 +22,7 @@ public extension TISInputSource {
|
||||||
else {
|
else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
return tsInputModeListKey.keys.map(\.description)
|
return tsInputModeListKey.keys.map { $0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult static func registerInputMethod() -> Bool {
|
@discardableResult static func registerInputMethod() -> Bool {
|
||||||
|
@ -85,6 +80,10 @@ public extension TISInputSource {
|
||||||
== kCFBooleanTrue
|
== kCFBooleanTrue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func generate(from identifier: String) -> TISInputSource? {
|
||||||
|
TISInputSource.rawTISInputSources(onlyASCII: false)[identifier]
|
||||||
|
}
|
||||||
|
|
||||||
var inputModeID: String {
|
var inputModeID: String {
|
||||||
unsafeBitCast(TISGetInputSourceProperty(self, kTISPropertyInputModeID), to: NSString.self) as String? ?? ""
|
unsafeBitCast(TISGetInputSourceProperty(self, kTISPropertyInputModeID), to: NSString.self) as String? ?? ""
|
||||||
}
|
}
|
||||||
|
@ -121,34 +120,9 @@ public extension TISInputSource {
|
||||||
return unsafeBitCast(r, to: NSString.self).integerValue as Int? ?? 0
|
return unsafeBitCast(r, to: NSString.self).integerValue as Int? ?? 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refactored by Shiki Suen.
|
static func rawTISInputSources(onlyASCII: Bool = false) -> [String: TISInputSource] {
|
||||||
static func match(identifiers: [String] = [], modeIDs: [String] = [], onlyASCII: Bool = false) -> [TISInputSource] {
|
|
||||||
let dicConditions: [CFString: Any] = !onlyASCII ? [:] : [
|
|
||||||
kTISPropertyInputSourceType: kTISTypeKeyboardLayout as CFString,
|
|
||||||
kTISPropertyInputSourceIsASCIICapable: kCFBooleanTrue as CFBoolean,
|
|
||||||
]
|
|
||||||
let cfDict = !onlyASCII ? nil : dicConditions as CFDictionary
|
|
||||||
var resultStack: [TISInputSource] = []
|
|
||||||
let unionedIDs = NSOrderedSet(array: modeIDs + identifiers).compactMap { $0 as? String }
|
|
||||||
let retrieved = (TISCreateInputSourceList(cfDict, true)?.takeRetainedValue() as? [TISInputSource]) ?? []
|
|
||||||
retrieved.forEach { tis in
|
|
||||||
unionedIDs.forEach { id in
|
|
||||||
guard tis.identifier == id || tis.inputModeID == id else { return }
|
|
||||||
if onlyASCII {
|
|
||||||
guard tis.scriptCode == 0 else { return }
|
|
||||||
}
|
|
||||||
resultStack.append(tis)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 為了保持指定排序,才在最後做這種處理。效能略有打折,但至少比起直接迭代容量破百的 retrieved 要好多了。
|
|
||||||
return unionedIDs.compactMap { currentIdentifier in
|
|
||||||
retrieved.first { $0.identifier == currentIdentifier || $0.inputModeID == currentIdentifier }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 備註:這是 Mzp 的原版函式,留在這裡當範本參考。上述的 .match() 函式都衍生自此。
|
|
||||||
static func rawTISInputSources(onlyASCII: Bool = false) -> [TISInputSource] {
|
|
||||||
// 為了指定檢索條件,先構築 CFDictionary 辭典。
|
// 為了指定檢索條件,先構築 CFDictionary 辭典。
|
||||||
|
// 第二項代指辭典容量。
|
||||||
let dicConditions: [CFString: Any] = !onlyASCII ? [:] : [
|
let dicConditions: [CFString: Any] = !onlyASCII ? [:] : [
|
||||||
kTISPropertyInputSourceType: kTISTypeKeyboardLayout as CFString,
|
kTISPropertyInputSourceType: kTISTypeKeyboardLayout as CFString,
|
||||||
kTISPropertyInputSourceIsASCIICapable: kCFBooleanTrue as CFBoolean,
|
kTISPropertyInputSourceIsASCIICapable: kCFBooleanTrue as CFBoolean,
|
||||||
|
@ -158,21 +132,10 @@ public extension TISInputSource {
|
||||||
if onlyASCII {
|
if onlyASCII {
|
||||||
result = result.filter { $0.scriptCode == 0 }
|
result = result.filter { $0.scriptCode == 0 }
|
||||||
}
|
}
|
||||||
return result
|
var resultDictionary: [String: TISInputSource] = [:]
|
||||||
}
|
|
||||||
|
|
||||||
/// Derived from rawTISInputSources().
|
|
||||||
static func getAllTISInputKeyboardLayoutMap() -> [String: TISInputSource.KeyboardLayout] {
|
|
||||||
// 為了指定檢索條件,先構築 CFDictionary 辭典。
|
|
||||||
let dicConditions: [CFString: Any] = [kTISPropertyInputSourceType: kTISTypeKeyboardLayout as CFString]
|
|
||||||
// 返回鍵盤配列清單。
|
|
||||||
let result = TISCreateInputSourceList(dicConditions as CFDictionary, true)?.takeRetainedValue() as? [TISInputSource] ?? .init()
|
|
||||||
var resultDictionary: [String: TISInputSource.KeyboardLayout] = [:]
|
|
||||||
result.forEach {
|
result.forEach {
|
||||||
let newNeta1 = TISInputSource.KeyboardLayout(id: $0.inputModeID, titleLocalized: $0.vChewingLocalizedName)
|
resultDictionary[$0.inputModeID] = $0
|
||||||
let newNeta2 = TISInputSource.KeyboardLayout(id: $0.identifier, titleLocalized: $0.vChewingLocalizedName)
|
resultDictionary[$0.identifier] = $0
|
||||||
resultDictionary[$0.inputModeID] = newNeta1
|
|
||||||
resultDictionary[$0.identifier] = newNeta2
|
|
||||||
}
|
}
|
||||||
return resultDictionary
|
return resultDictionary
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,212 +0,0 @@
|
||||||
---
|
|
||||||
# BasedOnStyle: Google
|
|
||||||
AccessModifierOffset: -1
|
|
||||||
AlignAfterOpenBracket: Align
|
|
||||||
AlignArrayOfStructures: None
|
|
||||||
AlignConsecutiveMacros: None
|
|
||||||
AlignConsecutiveAssignments: None
|
|
||||||
AlignConsecutiveBitFields: None
|
|
||||||
AlignConsecutiveDeclarations: None
|
|
||||||
AlignEscapedNewlines: Left
|
|
||||||
AlignOperands: Align
|
|
||||||
AlignTrailingComments: true
|
|
||||||
AllowAllArgumentsOnNextLine: true
|
|
||||||
AllowAllConstructorInitializersOnNextLine: true
|
|
||||||
AllowAllParametersOfDeclarationOnNextLine: true
|
|
||||||
AllowShortEnumsOnASingleLine: true
|
|
||||||
AllowShortBlocksOnASingleLine: Never
|
|
||||||
AllowShortCaseLabelsOnASingleLine: false
|
|
||||||
AllowShortFunctionsOnASingleLine: All
|
|
||||||
AllowShortLambdasOnASingleLine: All
|
|
||||||
AllowShortIfStatementsOnASingleLine: WithoutElse
|
|
||||||
AllowShortLoopsOnASingleLine: true
|
|
||||||
AlwaysBreakAfterDefinitionReturnType: None
|
|
||||||
AlwaysBreakAfterReturnType: None
|
|
||||||
AlwaysBreakBeforeMultilineStrings: true
|
|
||||||
AlwaysBreakTemplateDeclarations: Yes
|
|
||||||
AttributeMacros:
|
|
||||||
- __capability
|
|
||||||
BinPackArguments: true
|
|
||||||
BinPackParameters: true
|
|
||||||
BraceWrapping:
|
|
||||||
AfterCaseLabel: false
|
|
||||||
AfterClass: false
|
|
||||||
AfterControlStatement: Never
|
|
||||||
AfterEnum: false
|
|
||||||
AfterFunction: false
|
|
||||||
AfterNamespace: false
|
|
||||||
AfterObjCDeclaration: false
|
|
||||||
AfterStruct: false
|
|
||||||
AfterUnion: false
|
|
||||||
AfterExternBlock: false
|
|
||||||
BeforeCatch: false
|
|
||||||
BeforeElse: false
|
|
||||||
BeforeLambdaBody: false
|
|
||||||
BeforeWhile: false
|
|
||||||
IndentBraces: false
|
|
||||||
SplitEmptyFunction: true
|
|
||||||
SplitEmptyRecord: true
|
|
||||||
SplitEmptyNamespace: true
|
|
||||||
BreakBeforeBinaryOperators: None
|
|
||||||
BreakBeforeConceptDeclarations: true
|
|
||||||
BreakBeforeBraces: Attach
|
|
||||||
BreakBeforeInheritanceComma: false
|
|
||||||
BreakInheritanceList: BeforeColon
|
|
||||||
BreakBeforeTernaryOperators: true
|
|
||||||
BreakConstructorInitializersBeforeComma: false
|
|
||||||
BreakConstructorInitializers: BeforeColon
|
|
||||||
BreakAfterJavaFieldAnnotations: false
|
|
||||||
BreakStringLiterals: true
|
|
||||||
ColumnLimit: 80
|
|
||||||
CommentPragmas: "^ IWYU pragma:"
|
|
||||||
CompactNamespaces: false
|
|
||||||
ConstructorInitializerAllOnOneLineOrOnePerLine: true
|
|
||||||
ConstructorInitializerIndentWidth: 4
|
|
||||||
ContinuationIndentWidth: 4
|
|
||||||
Cpp11BracedListStyle: true
|
|
||||||
DeriveLineEnding: true
|
|
||||||
DerivePointerAlignment: true
|
|
||||||
DisableFormat: false
|
|
||||||
EmptyLineAfterAccessModifier: Never
|
|
||||||
EmptyLineBeforeAccessModifier: LogicalBlock
|
|
||||||
ExperimentalAutoDetectBinPacking: false
|
|
||||||
FixNamespaceComments: true
|
|
||||||
ForEachMacros:
|
|
||||||
- foreach
|
|
||||||
- Q_FOREACH
|
|
||||||
- BOOST_FOREACH
|
|
||||||
IfMacros:
|
|
||||||
- KJ_IF_MAYBE
|
|
||||||
IncludeBlocks: Regroup
|
|
||||||
IncludeCategories:
|
|
||||||
- Regex: '^<ext/.*\.h>'
|
|
||||||
Priority: 2
|
|
||||||
SortPriority: 0
|
|
||||||
CaseSensitive: false
|
|
||||||
- Regex: '^<.*\.h>'
|
|
||||||
Priority: 1
|
|
||||||
SortPriority: 0
|
|
||||||
CaseSensitive: false
|
|
||||||
- Regex: "^<.*"
|
|
||||||
Priority: 2
|
|
||||||
SortPriority: 0
|
|
||||||
CaseSensitive: false
|
|
||||||
- Regex: ".*"
|
|
||||||
Priority: 3
|
|
||||||
SortPriority: 0
|
|
||||||
CaseSensitive: false
|
|
||||||
IncludeIsMainRegex: "([-_](test|unittest))?$"
|
|
||||||
IncludeIsMainSourceRegex: ""
|
|
||||||
IndentAccessModifiers: false
|
|
||||||
IndentCaseLabels: true
|
|
||||||
IndentCaseBlocks: false
|
|
||||||
IndentGotoLabels: true
|
|
||||||
IndentPPDirectives: None
|
|
||||||
IndentExternBlock: AfterExternBlock
|
|
||||||
IndentRequires: false
|
|
||||||
IndentWidth: 2
|
|
||||||
IndentWrappedFunctionNames: false
|
|
||||||
InsertTrailingCommas: None
|
|
||||||
JavaScriptQuotes: Leave
|
|
||||||
JavaScriptWrapImports: true
|
|
||||||
KeepEmptyLinesAtTheStartOfBlocks: false
|
|
||||||
LambdaBodyIndentation: Signature
|
|
||||||
MacroBlockBegin: ""
|
|
||||||
MacroBlockEnd: ""
|
|
||||||
MaxEmptyLinesToKeep: 1
|
|
||||||
NamespaceIndentation: None
|
|
||||||
ObjCBinPackProtocolList: Never
|
|
||||||
ObjCBlockIndentWidth: 2
|
|
||||||
ObjCBreakBeforeNestedBlockParam: true
|
|
||||||
ObjCSpaceAfterProperty: false
|
|
||||||
ObjCSpaceBeforeProtocolList: true
|
|
||||||
PenaltyBreakAssignment: 2
|
|
||||||
PenaltyBreakBeforeFirstCallParameter: 1
|
|
||||||
PenaltyBreakComment: 300
|
|
||||||
PenaltyBreakFirstLessLess: 120
|
|
||||||
PenaltyBreakString: 1000
|
|
||||||
PenaltyBreakTemplateDeclaration: 10
|
|
||||||
PenaltyExcessCharacter: 1000000
|
|
||||||
PenaltyReturnTypeOnItsOwnLine: 200
|
|
||||||
PenaltyIndentedWhitespace: 0
|
|
||||||
PointerAlignment: Left
|
|
||||||
PPIndentWidth: -1
|
|
||||||
RawStringFormats:
|
|
||||||
- Language: Cpp
|
|
||||||
Delimiters:
|
|
||||||
- cc
|
|
||||||
- CC
|
|
||||||
- cpp
|
|
||||||
- Cpp
|
|
||||||
- CPP
|
|
||||||
- "c++"
|
|
||||||
- "C++"
|
|
||||||
- "cs"
|
|
||||||
CanonicalDelimiter: ""
|
|
||||||
BasedOnStyle: google
|
|
||||||
- Language: TextProto
|
|
||||||
Delimiters:
|
|
||||||
- pb
|
|
||||||
- PB
|
|
||||||
- proto
|
|
||||||
- PROTO
|
|
||||||
EnclosingFunctions:
|
|
||||||
- EqualsProto
|
|
||||||
- EquivToProto
|
|
||||||
- PARSE_PARTIAL_TEXT_PROTO
|
|
||||||
- PARSE_TEST_PROTO
|
|
||||||
- PARSE_TEXT_PROTO
|
|
||||||
- ParseTextOrDie
|
|
||||||
- ParseTextProtoOrDie
|
|
||||||
- ParseTestProto
|
|
||||||
- ParsePartialTestProto
|
|
||||||
CanonicalDelimiter: pb
|
|
||||||
BasedOnStyle: google
|
|
||||||
ReferenceAlignment: Pointer
|
|
||||||
ReflowComments: true
|
|
||||||
ShortNamespaceLines: 1
|
|
||||||
SortIncludes: CaseSensitive
|
|
||||||
SortJavaStaticImport: Before
|
|
||||||
SortUsingDeclarations: true
|
|
||||||
SpaceAfterCStyleCast: false
|
|
||||||
SpaceAfterLogicalNot: false
|
|
||||||
SpaceAfterTemplateKeyword: true
|
|
||||||
SpaceBeforeAssignmentOperators: true
|
|
||||||
SpaceBeforeCaseColon: false
|
|
||||||
SpaceBeforeCpp11BracedList: false
|
|
||||||
SpaceBeforeCtorInitializerColon: true
|
|
||||||
SpaceBeforeInheritanceColon: true
|
|
||||||
SpaceBeforeParens: ControlStatements
|
|
||||||
SpaceAroundPointerQualifiers: Default
|
|
||||||
SpaceBeforeRangeBasedForLoopColon: true
|
|
||||||
SpaceInEmptyBlock: false
|
|
||||||
SpaceInEmptyParentheses: false
|
|
||||||
SpacesBeforeTrailingComments: 2
|
|
||||||
SpacesInAngles: Never
|
|
||||||
SpacesInConditionalStatement: false
|
|
||||||
SpacesInContainerLiterals: true
|
|
||||||
SpacesInCStyleCastParentheses: false
|
|
||||||
SpacesInLineCommentPrefix:
|
|
||||||
Minimum: 1
|
|
||||||
Maximum: -1
|
|
||||||
SpacesInParentheses: false
|
|
||||||
SpacesInSquareBrackets: false
|
|
||||||
SpaceBeforeSquareBrackets: false
|
|
||||||
BitFieldColonSpacing: Both
|
|
||||||
Standard: Auto
|
|
||||||
StatementAttributeLikeMacros:
|
|
||||||
- Q_EMIT
|
|
||||||
StatementMacros:
|
|
||||||
- Q_UNUSED
|
|
||||||
- QT_REQUIRE_VERSION
|
|
||||||
TabWidth: 8
|
|
||||||
UseCRLF: false
|
|
||||||
UseTab: Never
|
|
||||||
WhitespaceSensitiveMacros:
|
|
||||||
- STRINGIZE
|
|
||||||
- PP_STRINGIZE
|
|
||||||
- BOOST_PP_STRINGIZE
|
|
||||||
- NS_SWIFT_NAME
|
|
||||||
- CF_SWIFT_NAME
|
|
||||||
---
|
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
.DS_Store
|
|
||||||
/.build
|
|
||||||
/Packages
|
|
||||||
xcuserdata/
|
|
||||||
DerivedData/
|
|
||||||
.swiftpm/configuration/registries.json
|
|
||||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
|
||||||
.netrc
|
|
|
@ -1,31 +0,0 @@
|
||||||
// swift-tools-version: 5.9
|
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
|
||||||
|
|
||||||
import PackageDescription
|
|
||||||
|
|
||||||
let package = Package(
|
|
||||||
name: "KimoDataReader",
|
|
||||||
products: [
|
|
||||||
// Products define the executables and libraries a package produces, making them visible to other packages.
|
|
||||||
.library(
|
|
||||||
name: "KimoDataReader",
|
|
||||||
targets: ["KimoDataReader"]
|
|
||||||
),
|
|
||||||
],
|
|
||||||
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: "ObjcKimoCommunicator",
|
|
||||||
publicHeadersPath: "include"
|
|
||||||
),
|
|
||||||
.target(
|
|
||||||
name: "KimoDataReader",
|
|
||||||
dependencies: ["ObjcKimoCommunicator"]
|
|
||||||
),
|
|
||||||
.testTarget(
|
|
||||||
name: "KimoDataReaderTests",
|
|
||||||
dependencies: ["KimoDataReader"]
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
|
|
@ -1,19 +0,0 @@
|
||||||
# KimoCommunicator
|
|
||||||
|
|
||||||
用來與奇摩輸入法進行 NSConnection 通訊的模組,便於直接從奇摩輸入法讀入使用者自訂詞資料庫的資料。
|
|
||||||
|
|
||||||
> 免責聲明:
|
|
||||||
> 與奇摩輸入法有關的原始碼是由 Yahoo 奇摩以 `SPDX Identifier: BSD-3-Clause` 釋出的,
|
|
||||||
> 但敝模組只是藉由其 Protocol API 與該當程式進行跨執行緒通訊,所以屬於合理使用範圍。
|
|
||||||
|
|
||||||
```
|
|
||||||
// (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.
|
|
||||||
```
|
|
||||||
|
|
||||||
$ EOF.
|
|
|
@ -1,25 +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 ObjcKimoCommunicator
|
|
||||||
|
|
||||||
public class KimoCommunicator: ObjcKimoCommunicator {
|
|
||||||
public static let shared: KimoCommunicator = .init()
|
|
||||||
|
|
||||||
public func prepareData(handler: @escaping (_ key: String, _ value: String) -> Void) {
|
|
||||||
guard KimoCommunicator.shared.establishConnection() else { return }
|
|
||||||
assert(KimoCommunicator.shared.hasValidConnection())
|
|
||||||
let loopAmount = KimoCommunicator.shared.userPhraseDBTotalAmountOfRows()
|
|
||||||
for i in 0 ..< loopAmount {
|
|
||||||
let fetched = KimoCommunicator.shared.userPhraseDBDictionary(atRow: i)
|
|
||||||
guard let key = fetched["BPMF"], let text = fetched["Text"] else { continue }
|
|
||||||
handler(key, text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,86 +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.
|
|
||||||
|
|
||||||
// 免責聲明:
|
|
||||||
// 與奇摩輸入法有關的原始碼是由 Yahoo 奇摩以 `SPDX Identifier: BSD-3-Clause` 釋出的,
|
|
||||||
// 但敝模組只是藉由其 Protocol API 與該當程式進行跨執行緒通訊,所以屬於合理使用範圍。
|
|
||||||
|
|
||||||
#import "KimoCommunicator.h"
|
|
||||||
|
|
||||||
#import <AppKit/AppKit.h>
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
|
|
||||||
#define kYahooKimoDataObjectConnectionName @"YahooKeyKeyService"
|
|
||||||
|
|
||||||
@implementation ObjcKimoCommunicator {
|
|
||||||
id _xpcConnection;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 解構。
|
|
||||||
- (void)dealloc {
|
|
||||||
[self disconnect];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 斷開連線。
|
|
||||||
- (void)disconnect {
|
|
||||||
_xpcConnection = nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 嘗試連線。
|
|
||||||
- (bool)establishConnection {
|
|
||||||
// 奇摩輸入法2012最終版在建置的時候還沒用到 NSXPCConnection,實質上並不支援
|
|
||||||
// NSXPCConnection。 因此,這裡使用 NSXPCConnection 的話反而會壞事。
|
|
||||||
#pragma GCC diagnostic push
|
|
||||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
|
||||||
_xpcConnection = [NSConnection rootProxyForConnectionWithRegisteredName:
|
|
||||||
kYahooKimoDataObjectConnectionName
|
|
||||||
host:nil];
|
|
||||||
#pragma GCC diagnostic pop
|
|
||||||
BOOL result = false;
|
|
||||||
if (_xpcConnection) {
|
|
||||||
result = true;
|
|
||||||
}
|
|
||||||
if (result) {
|
|
||||||
[_xpcConnection setProtocolForProxy:@protocol(KimoUserDataReaderService)];
|
|
||||||
NSLog(@"vChewingDebug: Connection successful. Available data amount: %d.\n",
|
|
||||||
[_xpcConnection userPhraseDBNumberOfRow]);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 偵測連線是否有效。
|
|
||||||
- (bool)hasValidConnection {
|
|
||||||
BOOL result = false;
|
|
||||||
if (_xpcConnection) result = true;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)userPhraseDBCanProvideService {
|
|
||||||
return [self hasValidConnection]
|
|
||||||
? [_xpcConnection userPhraseDBCanProvideService]
|
|
||||||
: NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (int)userPhraseDBTotalAmountOfRows {
|
|
||||||
return [self hasValidConnection] ? [_xpcConnection userPhraseDBNumberOfRow]
|
|
||||||
: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSDictionary<NSString*, NSString*> *)userPhraseDBDictionaryAtRow:(int)row {
|
|
||||||
return [self hasValidConnection]
|
|
||||||
? [_xpcConnection userPhraseDBDictionaryAtRow:row]
|
|
||||||
: [NSDictionary alloc];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (bool)exportUserPhraseDBToFile:(NSString *)path {
|
|
||||||
return [self hasValidConnection]
|
|
||||||
? [_xpcConnection exportUserPhraseDBToFile:path]
|
|
||||||
: NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
|
@ -1,46 +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.
|
|
||||||
|
|
||||||
// 免責聲明:
|
|
||||||
// 與奇摩輸入法有關的原始碼是由 Yahoo 奇摩以 `SPDX Identifier: BSD-3-Clause` 釋出的,
|
|
||||||
// 但敝模組只是藉由其 Protocol API 與該當程式進行跨執行緒通訊,所以屬於合理使用範圍。
|
|
||||||
|
|
||||||
#import <AppKit/AppKit.h>
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
|
||||||
|
|
||||||
@protocol KimoUserDataReaderService
|
|
||||||
- (BOOL)userPhraseDBCanProvideService;
|
|
||||||
- (int)userPhraseDBNumberOfRow;
|
|
||||||
- (NSDictionary *)userPhraseDBDictionaryAtRow:(int)row;
|
|
||||||
- (bool)exportUserPhraseDBToFile:(NSString *)path;
|
|
||||||
@end
|
|
||||||
|
|
||||||
/// 不要理會 Xcode 對 NSDistantObject 的過期狗吠。
|
|
||||||
/// 奇摩輸入法是用 NSConnection 寫的,
|
|
||||||
/// 換用 NSXPCConnection 只會製造更多的問題。
|
|
||||||
@interface ObjcKimoCommunicator : NSObject
|
|
||||||
|
|
||||||
/// 嘗試連線。
|
|
||||||
- (bool)establishConnection;
|
|
||||||
|
|
||||||
/// 偵測連線是否有效。
|
|
||||||
- (bool)hasValidConnection;
|
|
||||||
|
|
||||||
/// 斷開連線。
|
|
||||||
- (void)disconnect;
|
|
||||||
|
|
||||||
// Conforming KimoUserDataReaderService protocol.
|
|
||||||
- (BOOL)userPhraseDBCanProvideService;
|
|
||||||
- (int)userPhraseDBTotalAmountOfRows;
|
|
||||||
- (NSDictionary<NSString*, NSString*> *)userPhraseDBDictionaryAtRow:(int)row;
|
|
||||||
- (bool)exportUserPhraseDBToFile:(NSString *)path;
|
|
||||||
@end
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
|
|
@ -1,10 +0,0 @@
|
||||||
@testable import ObjcKimoCommunicator
|
|
||||||
import XCTest
|
|
||||||
|
|
||||||
final class KimoDataReaderTests: XCTestCase {
|
|
||||||
// 先運行奇摩輸入法,再跑這個測試。
|
|
||||||
func testExample() throws {
|
|
||||||
let shared = ObjcKimoCommunicator()
|
|
||||||
print(shared.establishConnection())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
find . -regex '.*\.\(hh\)' -exec clang-format -style=file -i {} \;
|
|
||||||
find . -regex '.*\.\(cc\)' -exec clang-format -style=file -i {} \;
|
|
||||||
find . -regex '.*\.\(mm\)' -exec clang-format -style=file -i {} \;
|
|
||||||
find . -regex '.*\.\(h\)' -exec clang-format -style=file -i {} \;
|
|
||||||
find . -regex '.*\.\(c\)' -exec clang-format -style=file -i {} \;
|
|
||||||
find . -regex '.*\.\(m\)' -exec clang-format -style=file -i {} \;
|
|
|
@ -15,7 +15,8 @@ let package = Package(
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(path: "../RMJay_LineReader"),
|
.package(path: "../RMJay_LineReader"),
|
||||||
.package(path: "../vChewing_Megrez"),
|
.package(path: "../vChewing_Megrez"),
|
||||||
.package(path: "../vChewing_SwiftExtension"),
|
.package(path: "../vChewing_PinyinPhonaConverter"),
|
||||||
|
.package(path: "../vChewing_Shared"),
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.target(
|
.target(
|
||||||
|
@ -23,7 +24,12 @@ let package = Package(
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.product(name: "LineReader", package: "RMJay_LineReader"),
|
.product(name: "LineReader", package: "RMJay_LineReader"),
|
||||||
.product(name: "Megrez", package: "vChewing_Megrez"),
|
.product(name: "Megrez", package: "vChewing_Megrez"),
|
||||||
.product(name: "SwiftExtension", package: "vChewing_SwiftExtension"),
|
.product(name: "Shared", package: "vChewing_Shared"),
|
||||||
|
.product(name: "PinyinPhonaConverter", package: "vChewing_PinyinPhonaConverter"),
|
||||||
|
],
|
||||||
|
resources: [
|
||||||
|
.process("Resources/sequenceDataFromEtenDOS-chs.json"),
|
||||||
|
.process("Resources/sequenceDataFromEtenDOS-cht.json"),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
# LangModelAssembly
|
# LangModelAssembly
|
||||||
|
|
||||||
威注音輸入法的語言模組總成套裝,以 LMAssembly 命名空間承載下述唯二對外物件:
|
威注音輸入法的語言模組總成套裝。
|
||||||
|
|
||||||
|
- vChewingLM:總命名空間,也承載一些在套裝內共用的工具函式。
|
||||||
- LMConsolidator:自動格式整理模組。
|
- LMConsolidator:自動格式整理模組。
|
||||||
- LMInstantiator:語言模組副本化模組,亦集成一些自身功能擴展。
|
- LMInstantiator:語言模組副本化模組。另有其日期時間擴充模組可用(對 CIN 磁帶模式無效)。
|
||||||
|
|
||||||
LMAssembly 總命名空間也承載一些在套裝內共用的工具函式。
|
|
||||||
|
|
||||||
以下是子模組:
|
以下是子模組:
|
||||||
|
|
||||||
- LMAssociates:關聯詞語模組。
|
|
||||||
- lmCassette:專門用來處理 CIN 磁帶檔案的模組,命名為「遠野」引擎。
|
- lmCassette:專門用來處理 CIN 磁帶檔案的模組,命名為「遠野」引擎。
|
||||||
|
- LMAssociates:關聯詞語模組。
|
||||||
- LMCoreEX:可以直接讀取 TXT 格式的帶有權重資料的語彙檔案的模組。
|
- LMCoreEX:可以直接讀取 TXT 格式的帶有權重資料的語彙檔案的模組。
|
||||||
|
- LMCoreJSON:專門用來讀取原廠 JSON 檔案的模組。
|
||||||
- lmPlainBopomofo:專門用來讀取使用者自訂ㄅ半候選字順序覆蓋定義檔案(plist)的模組。
|
- lmPlainBopomofo:專門用來讀取使用者自訂ㄅ半候選字順序覆蓋定義檔案(plist)的模組。
|
||||||
- lmReplacements:專門用來讀取使用者語彙置換模式的辭典資料的模組。
|
- lmReplacements:專門用來讀取使用者語彙置換模式的辭典資料的模組。
|
||||||
- lmUserOverride:半衰記憶模組。
|
- lmUserOverride:半衰記憶模組。
|
||||||
|
|
|
@ -11,8 +11,7 @@ import Foundation
|
||||||
/// 工作原理:先用 InputToken.parse 分析原始字串,給出準確的 Token。
|
/// 工作原理:先用 InputToken.parse 分析原始字串,給出準確的 Token。
|
||||||
/// 然後再讓這個 Token 用 .translated() 自我表述出轉換結果。
|
/// 然後再讓這個 Token 用 .translated() 自我表述出轉換結果。
|
||||||
|
|
||||||
extension LMAssembly {
|
public enum InputToken {
|
||||||
enum InputToken {
|
|
||||||
case timeZone(shortened: Bool)
|
case timeZone(shortened: Bool)
|
||||||
case timeNow(shortened: Bool)
|
case timeNow(shortened: Bool)
|
||||||
case date(dayDelta: Int = 0, yearDelta: Int = 0, shortened: Bool = true, luna: Bool = false)
|
case date(dayDelta: Int = 0, yearDelta: Int = 0, shortened: Bool = true, luna: Bool = false)
|
||||||
|
@ -21,21 +20,20 @@ extension LMAssembly {
|
||||||
case yearGanzhi(yearDelta: Int = 0)
|
case yearGanzhi(yearDelta: Int = 0)
|
||||||
case yearZodiac(yearDelta: Int = 0)
|
case yearZodiac(yearDelta: Int = 0)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 正式對外投入使用的 API。
|
// MARK: - 正式對外投入使用的 API。
|
||||||
|
|
||||||
public extension String {
|
public extension String {
|
||||||
func parseAsInputToken(isCHS: Bool) -> [String] {
|
func parseAsInputToken(isCHS: Bool) -> [String] {
|
||||||
LMAssembly.InputToken.parse(from: self).map { $0.translated(isCHS: isCHS) }.flatMap { $0 }.deduplicated
|
InputToken.parse(from: self).map { $0.translated(isCHS: isCHS) }.flatMap { $0 }.deduplicated
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Parser parsing raw token value to construct token.
|
// MARK: - Parser parsing raw token value to construct token.
|
||||||
|
|
||||||
extension LMAssembly.InputToken {
|
public extension InputToken {
|
||||||
static func parse(from rawToken: String) -> [LMAssembly.InputToken] {
|
static func parse(from rawToken: String) -> [InputToken] {
|
||||||
var result: [LMAssembly.InputToken] = []
|
var result: [InputToken] = []
|
||||||
guard rawToken.prefix(6) == "MACRO@" else { return result }
|
guard rawToken.prefix(6) == "MACRO@" else { return result }
|
||||||
var mapParams: [String: Int] = [:]
|
var mapParams: [String: Int] = [:]
|
||||||
let tokenComponents = rawToken.dropFirst(6).split(separator: "_").map { param in
|
let tokenComponents = rawToken.dropFirst(6).split(separator: "_").map { param in
|
||||||
|
@ -71,7 +69,7 @@ extension LMAssembly.InputToken {
|
||||||
|
|
||||||
// MARK: - Parser parsing token itself.
|
// MARK: - Parser parsing token itself.
|
||||||
|
|
||||||
extension LMAssembly.InputToken {
|
public extension InputToken {
|
||||||
func translated(isCHS: Bool) -> [String] {
|
func translated(isCHS: Bool) -> [String] {
|
||||||
let locale = Locale(identifier: isCHS ? "zh-Hans" : "zh-Hant-TW")
|
let locale = Locale(identifier: isCHS ? "zh-Hans" : "zh-Hant-TW")
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
|
|
|
@ -8,8 +8,9 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import LineReader
|
import LineReader
|
||||||
|
import Shared
|
||||||
|
|
||||||
public extension LMAssembly {
|
public extension vChewingLM {
|
||||||
enum LMConsolidator {
|
enum LMConsolidator {
|
||||||
public static let kPragmaHeader = "# 𝙵𝙾𝚁𝙼𝙰𝚃 𝚘𝚛𝚐.𝚊𝚝𝚎𝚕𝚒𝚎𝚛𝙸𝚗𝚖𝚞.𝚟𝚌𝚑𝚎𝚠𝚒𝚗𝚐.𝚞𝚜𝚎𝚛𝙻𝚊𝚗𝚐𝚞𝚊𝚐𝚎𝙼𝚘𝚍𝚎𝚕𝙳𝚊𝚝𝚊.𝚏𝚘𝚛𝚖𝚊𝚝𝚝𝚎𝚍"
|
public static let kPragmaHeader = "# 𝙵𝙾𝚁𝙼𝙰𝚃 𝚘𝚛𝚐.𝚊𝚝𝚎𝚕𝚒𝚎𝚛𝙸𝚗𝚖𝚞.𝚟𝚌𝚑𝚎𝚠𝚒𝚗𝚐.𝚞𝚜𝚎𝚛𝙻𝚊𝚗𝚐𝚞𝚊𝚐𝚎𝙼𝚘𝚍𝚎𝚕𝙳𝚊𝚝𝚊.𝚏𝚘𝚛𝚖𝚊𝚝𝚝𝚎𝚍"
|
||||||
|
|
||||||
|
@ -25,19 +26,19 @@ public extension LMAssembly {
|
||||||
let lineReader = try LineReader(file: fileHandle)
|
let lineReader = try LineReader(file: fileHandle)
|
||||||
for strLine in lineReader { // 不需要 i=0,因為第一遍迴圈就出結果。
|
for strLine in lineReader { // 不需要 i=0,因為第一遍迴圈就出結果。
|
||||||
if strLine != kPragmaHeader {
|
if strLine != kPragmaHeader {
|
||||||
vCLMLog("Header Mismatch, Starting In-Place Consolidation.")
|
vCLog("Header Mismatch, Starting In-Place Consolidation.")
|
||||||
return false
|
return false
|
||||||
} else {
|
} else {
|
||||||
vCLMLog("Header Verification Succeeded: \(strLine).")
|
vCLog("Header Verification Succeeded: \(strLine).")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
vCLMLog("Header Verification Failed: File Access Error.")
|
vCLog("Header Verification Failed: File Access Error.")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
vCLMLog("Header Verification Failed: File Missing.")
|
vCLog("Header Verification Failed: File Missing.")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,12 +51,12 @@ public extension LMAssembly {
|
||||||
let dict = try FileManager.default.attributesOfItem(atPath: path)
|
let dict = try FileManager.default.attributesOfItem(atPath: path)
|
||||||
if let value = dict[FileAttributeKey.size] as? UInt64 { fileSize = value }
|
if let value = dict[FileAttributeKey.size] as? UInt64 { fileSize = value }
|
||||||
} catch {
|
} catch {
|
||||||
vCLMLog("EOF Fix Failed: File Missing at \(path).")
|
vCLog("EOF Fix Failed: File Missing at \(path).")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
guard let fileSize = fileSize else { return false }
|
guard let fileSize = fileSize else { return false }
|
||||||
guard let writeFile = FileHandle(forUpdatingAtPath: path) else {
|
guard let writeFile = FileHandle(forUpdatingAtPath: path) else {
|
||||||
vCLMLog("EOF Fix Failed: File Not Writable at \(path).")
|
vCLog("EOF Fix Failed: File Not Writable at \(path).")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
defer { writeFile.closeFile() }
|
defer { writeFile.closeFile() }
|
||||||
|
@ -63,11 +64,11 @@ public extension LMAssembly {
|
||||||
/// 但這個函式執行完之後往往就會 consolidate() 整理格式,所以不會有差。
|
/// 但這個函式執行完之後往往就會 consolidate() 整理格式,所以不會有差。
|
||||||
writeFile.seek(toFileOffset: fileSize - 1)
|
writeFile.seek(toFileOffset: fileSize - 1)
|
||||||
if writeFile.readDataToEndOfFile().first != 0x0A {
|
if writeFile.readDataToEndOfFile().first != 0x0A {
|
||||||
vCLMLog("EOF Missing Confirmed, Start Fixing.")
|
vCLog("EOF Missing Confirmed, Start Fixing.")
|
||||||
var newData = Data()
|
var newData = Data()
|
||||||
newData.append(0x0A)
|
newData.append(0x0A)
|
||||||
writeFile.write(newData)
|
writeFile.write(newData)
|
||||||
vCLMLog("EOF Successfully Assured.")
|
vCLog("EOF Successfully Assured.")
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -141,29 +142,14 @@ public extension LMAssembly {
|
||||||
// Write consolidated file contents.
|
// Write consolidated file contents.
|
||||||
try strProcessed.write(to: urlPath, atomically: false, encoding: .utf8)
|
try strProcessed.write(to: urlPath, atomically: false, encoding: .utf8)
|
||||||
} catch {
|
} catch {
|
||||||
vCLMLog("Consolidation Failed w/ File: \(path), error: \(error)")
|
vCLog("Consolidation Failed w/ File: \(path), error: \(error)")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
vCLMLog("Either Consolidation Successful Or No-Need-To-Consolidate.")
|
vCLog("Either Consolidation Successful Or No-Need-To-Consolidate.")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
vCLMLog("Consolidation Failed: File Missing at \(path).")
|
vCLog("Consolidation Failed: File Missing at \(path).")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension String {
|
|
||||||
mutating func regReplace(pattern: String, replaceWith: String = "") {
|
|
||||||
// Ref: https://stackoverflow.com/a/40993403/4162914 && https://stackoverflow.com/a/71291137/4162914
|
|
||||||
do {
|
|
||||||
let regex = try NSRegularExpression(
|
|
||||||
pattern: pattern, options: [.caseInsensitive, .anchorsMatchLines]
|
|
||||||
)
|
|
||||||
let range = NSRange(startIndex..., in: self)
|
|
||||||
self = regex.stringByReplacingMatches(
|
|
||||||
in: self, options: [], range: range, withTemplate: replaceWith
|
|
||||||
)
|
|
||||||
} catch { return }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -8,8 +8,10 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Megrez
|
import Megrez
|
||||||
|
import Shared
|
||||||
|
import SQLite3
|
||||||
|
|
||||||
public extension LMAssembly {
|
public extension vChewingLM {
|
||||||
/// 語言模組副本化模組(LMInstantiator,下稱「LMI」)自身為符合天權星組字引擎內
|
/// 語言模組副本化模組(LMInstantiator,下稱「LMI」)自身為符合天權星組字引擎內
|
||||||
/// 的 LangModelProtocol 協定的模組、統籌且整理來自其它子模組的資料(包括使
|
/// 的 LangModelProtocol 協定的模組、統籌且整理來自其它子模組的資料(包括使
|
||||||
/// 用者語彙、繪文字模組、語彙濾除表、原廠語言模組等)。
|
/// 用者語彙、繪文字模組、語彙濾除表、原廠語言模組等)。
|
||||||
|
@ -37,40 +39,45 @@ public extension LMAssembly {
|
||||||
public var isCNSEnabled = false
|
public var isCNSEnabled = false
|
||||||
public var isSymbolEnabled = false
|
public var isSymbolEnabled = false
|
||||||
public var isSCPCEnabled = false
|
public var isSCPCEnabled = false
|
||||||
public var filterNonCNSReadings = false
|
|
||||||
public var deltaOfCalendarYears: Int = -2000
|
public var deltaOfCalendarYears: Int = -2000
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var asyncLoadingUserData: Bool = true
|
|
||||||
|
|
||||||
// SQLite 連線所在的記憶體位置。
|
// SQLite 連線所在的記憶體位置。
|
||||||
static var ptrSQL: OpaquePointer?
|
static var ptrSQL: OpaquePointer?
|
||||||
|
|
||||||
// SQLite 連線是否已經建立。
|
// SQLite 連線是否已經建立。
|
||||||
public internal(set) static var isSQLDBConnected: Bool = false
|
public private(set) static var isSQLDBConnected: Bool = false
|
||||||
|
|
||||||
// 簡體中文模型?
|
// 簡體中文模型?
|
||||||
public let isCHS: Bool
|
public let isCHS: Bool
|
||||||
|
|
||||||
// 在函式內部用以記錄狀態的開關。
|
// 在函式內部用以記錄狀態的開關。
|
||||||
public private(set) var config = Config()
|
public var config = Config()
|
||||||
|
|
||||||
// 這句需要留著,不然無法被 package 外界存取。
|
// 這句需要留著,不然無法被 package 外界存取。
|
||||||
public init(
|
public init(isCHS: Bool = false) {
|
||||||
isCHS: Bool = false,
|
|
||||||
uomDataURL: URL? = nil
|
|
||||||
) {
|
|
||||||
self.isCHS = isCHS
|
self.isCHS = isCHS
|
||||||
lmUserOverride = .init(dataURL: uomDataURL)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult public func setOptions(handler: (inout Config) -> Void) -> LMInstantiator {
|
public func setOptions(handler: (inout Config) -> Void) {
|
||||||
handler(&config)
|
handler(&config)
|
||||||
return self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func setCassetCandidateKeyValidator(_ validator: @escaping (String) -> Bool) {
|
@discardableResult public static func connectSQLDB(dbPath: String, dropPreviousConnection: Bool = true) -> Bool {
|
||||||
Self.lmCassette.candidateKeysValidator = validator
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 介紹一下幾個通用的語言模組型別:
|
/// 介紹一下幾個通用的語言模組型別:
|
||||||
|
@ -85,7 +92,6 @@ public extension LMAssembly {
|
||||||
|
|
||||||
// 磁帶資料模組。「currentCassette」對外唯讀,僅用來讀取磁帶本身的中繼資料(Metadata)。
|
// 磁帶資料模組。「currentCassette」對外唯讀,僅用來讀取磁帶本身的中繼資料(Metadata)。
|
||||||
static var lmCassette = LMCassette()
|
static var lmCassette = LMCassette()
|
||||||
static var lmPlainBopomofo = LMPlainBopomofo()
|
|
||||||
|
|
||||||
// 聲明使用者語言模組。
|
// 聲明使用者語言模組。
|
||||||
// 使用者語言模組使用多執行緒的話,可能會導致一些問題。有時間再仔細排查看看。
|
// 使用者語言模組使用多執行緒的話,可能會導致一些問題。有時間再仔細排查看看。
|
||||||
|
@ -100,46 +106,30 @@ public extension LMAssembly {
|
||||||
)
|
)
|
||||||
var lmReplacements = LMReplacements()
|
var lmReplacements = LMReplacements()
|
||||||
var lmAssociates = LMAssociates()
|
var lmAssociates = LMAssociates()
|
||||||
|
var lmPlainBopomofo = LMPlainBopomofo()
|
||||||
// 半衰记忆模组
|
|
||||||
var lmUserOverride: LMUserOverride
|
|
||||||
|
|
||||||
// MARK: - 工具函式
|
// MARK: - 工具函式
|
||||||
|
|
||||||
public func resetFactoryJSONModels() {}
|
public func resetFactoryJSONModels() {}
|
||||||
|
|
||||||
public func loadUserPhrasesData(path: String, filterPath: String?) {
|
public func loadUserPhrasesData(path: String, filterPath: String?) {
|
||||||
func loadMain() {
|
|
||||||
if FileManager.default.isReadableFile(atPath: path) {
|
|
||||||
lmUserPhrases.clear()
|
|
||||||
lmUserPhrases.open(path)
|
|
||||||
vCLMLog("lmUserPhrases: \(lmUserPhrases.count) entries of data loaded from: \(path)")
|
|
||||||
} else {
|
|
||||||
vCLMLog("lmUserPhrases: File access failure: \(path)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !Self.asyncLoadingUserData {
|
|
||||||
loadMain()
|
|
||||||
} else {
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
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)")
|
||||||
|
} else {
|
||||||
|
vCLog("lmUserPhrases: File access failure: \(path)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
guard let filterPath = filterPath else { return }
|
guard let filterPath = filterPath else { return }
|
||||||
func loadFilter() {
|
|
||||||
if FileManager.default.isReadableFile(atPath: filterPath) {
|
|
||||||
lmFiltered.clear()
|
|
||||||
lmFiltered.open(filterPath)
|
|
||||||
vCLMLog("lmFiltered: \(lmFiltered.count) entries of data loaded from: \(path)")
|
|
||||||
} else {
|
|
||||||
vCLMLog("lmFiltered: File access failure: \(path)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !Self.asyncLoadingUserData {
|
|
||||||
loadFilter()
|
|
||||||
} else {
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
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)")
|
||||||
|
} else {
|
||||||
|
vCLog("lmFiltered: File access failure: \(path)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -149,85 +139,74 @@ public extension LMAssembly {
|
||||||
if FileManager.default.isReadableFile(atPath: path) {
|
if FileManager.default.isReadableFile(atPath: path) {
|
||||||
lmFiltered.clear()
|
lmFiltered.clear()
|
||||||
lmFiltered.open(path)
|
lmFiltered.open(path)
|
||||||
vCLMLog("lmFiltered: \(lmFiltered.count) entries of data loaded from: \(path)")
|
vCLog("lmFiltered: \(lmFiltered.count) entries of data loaded from: \(path)")
|
||||||
} else {
|
} else {
|
||||||
vCLMLog("lmFiltered: File access failure: \(path)")
|
vCLog("lmFiltered: File access failure: \(path)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func loadUserSymbolData(path: String) {
|
public func loadUserSymbolData(path: String) {
|
||||||
func load() {
|
|
||||||
if FileManager.default.isReadableFile(atPath: path) {
|
|
||||||
lmUserSymbols.clear()
|
|
||||||
lmUserSymbols.open(path)
|
|
||||||
vCLMLog("lmUserSymbol: \(lmUserSymbols.count) entries of data loaded from: \(path)")
|
|
||||||
} else {
|
|
||||||
vCLMLog("lmUserSymbol: File access failure: \(path)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !Self.asyncLoadingUserData {
|
|
||||||
load()
|
|
||||||
} else {
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
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)")
|
||||||
|
} else {
|
||||||
|
vCLog("lmUserSymbol: File access failure: \(path)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func loadUserAssociatesData(path: String) {
|
public func loadUserAssociatesData(path: String) {
|
||||||
func load() {
|
|
||||||
if FileManager.default.isReadableFile(atPath: path) {
|
|
||||||
lmAssociates.clear()
|
|
||||||
lmAssociates.open(path)
|
|
||||||
vCLMLog("lmAssociates: \(lmAssociates.count) entries of data loaded from: \(path)")
|
|
||||||
} else {
|
|
||||||
vCLMLog("lmAssociates: File access failure: \(path)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !Self.asyncLoadingUserData {
|
|
||||||
load()
|
|
||||||
} else {
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
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)")
|
||||||
|
} else {
|
||||||
|
vCLog("lmAssociates: File access failure: \(path)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func loadReplacementsData(path: String) {
|
public func loadReplacementsData(path: String) {
|
||||||
func load() {
|
|
||||||
if FileManager.default.isReadableFile(atPath: path) {
|
|
||||||
lmReplacements.clear()
|
|
||||||
lmReplacements.open(path)
|
|
||||||
vCLMLog("lmReplacements: \(lmReplacements.count) entries of data loaded from: \(path)")
|
|
||||||
} else {
|
|
||||||
vCLMLog("lmReplacements: File access failure: \(path)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !Self.asyncLoadingUserData {
|
|
||||||
load()
|
|
||||||
} else {
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
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)")
|
||||||
|
} else {
|
||||||
|
vCLog("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)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var isCassetteDataLoaded: Bool { Self.lmCassette.isLoaded }
|
public var isCassetteDataLoaded: Bool { Self.lmCassette.isLoaded }
|
||||||
public static func loadCassetteData(path: String) {
|
public static func loadCassetteData(path: String) {
|
||||||
func load() {
|
DispatchQueue.main.async {
|
||||||
if FileManager.default.isReadableFile(atPath: path) {
|
if FileManager.default.isReadableFile(atPath: path) {
|
||||||
Self.lmCassette.clear()
|
Self.lmCassette.clear()
|
||||||
Self.lmCassette.open(path)
|
Self.lmCassette.open(path)
|
||||||
vCLMLog("lmCassette: \(Self.lmCassette.count) entries of data loaded from: \(path)")
|
vCLog("lmCassette: \(Self.lmCassette.count) entries of data loaded from: \(path)")
|
||||||
} else {
|
} else {
|
||||||
vCLMLog("lmCassette: File access failure: \(path)")
|
vCLog("lmCassette: File access failure: \(path)")
|
||||||
}
|
|
||||||
}
|
|
||||||
if !Self.asyncLoadingUserData {
|
|
||||||
load()
|
|
||||||
} else {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
load()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -353,27 +332,20 @@ public extension LMAssembly {
|
||||||
|
|
||||||
// 如果有檢測到使用者自訂逐字選字語料庫內的相關資料的話,在這裡先插入。
|
// 如果有檢測到使用者自訂逐字選字語料庫內的相關資料的話,在這裡先插入。
|
||||||
if config.isSCPCEnabled {
|
if config.isSCPCEnabled {
|
||||||
rawAllUnigrams += Self.lmPlainBopomofo.valuesFor(key: keyChain, isCHS: isCHS).map {
|
rawAllUnigrams += lmPlainBopomofo.valuesFor(key: keyChain).map { Megrez.Unigram(value: $0, score: 0) }
|
||||||
Megrez.Unigram(value: $0, score: 0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 用 reversed 指令讓使用者語彙檔案內的詞條優先順序隨著行數增加而逐漸增高。
|
||||||
|
// 這樣一來就可以在就地新增語彙時徹底複寫優先權。
|
||||||
|
// 將兩句差分也是為了讓 rawUserUnigrams 的類型不受可能的影響。
|
||||||
|
rawAllUnigrams += lmUserPhrases.unigramsFor(key: keyChain).reversed()
|
||||||
|
|
||||||
if !config.isCassetteEnabled || config.isCassetteEnabled && keyChain.map(\.description)[0] == "_" {
|
if !config.isCassetteEnabled || config.isCassetteEnabled && keyChain.map(\.description)[0] == "_" {
|
||||||
// 先給出 NumPad 的結果。
|
// 先給出 NumPad 的結果。
|
||||||
rawAllUnigrams += supplyNumPadUnigrams(key: keyChain)
|
rawAllUnigrams += supplyNumPadUnigrams(key: keyChain)
|
||||||
// LMMisc 與 LMCore 的 score 在 (-10.0, 0.0) 這個區間內。
|
// LMMisc 與 LMCore 的 score 在 (-10.0, 0.0) 這個區間內。
|
||||||
rawAllUnigrams += factoryUnigramsFor(key: keyChain, column: .theDataCHEW)
|
rawAllUnigrams += factoryUnigramsFor(key: keyChain, column: .theDataCHEW)
|
||||||
// 原廠核心辭典內容。
|
rawAllUnigrams += factoryCoreUnigramsFor(key: keyChain)
|
||||||
var coreUnigramsResult: [Megrez.Unigram] = factoryCoreUnigramsFor(key: keyChain)
|
|
||||||
// 如果是繁體中文、且有開啟 CNS11643 全字庫讀音過濾開關的話,對原廠核心辭典內容追加過濾處理:
|
|
||||||
if config.filterNonCNSReadings, !isCHS {
|
|
||||||
coreUnigramsResult.removeAll { thisUnigram in
|
|
||||||
!checkCNSConformation(for: thisUnigram, keyArray: keyArray)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 正式追加原廠核心辭典檢索結果。
|
|
||||||
rawAllUnigrams += coreUnigramsResult
|
|
||||||
|
|
||||||
if config.isCNSEnabled {
|
if config.isCNSEnabled {
|
||||||
rawAllUnigrams += factoryUnigramsFor(key: keyChain, column: .theDataCNS)
|
rawAllUnigrams += factoryUnigramsFor(key: keyChain, column: .theDataCNS)
|
||||||
}
|
}
|
||||||
|
@ -386,21 +358,6 @@ public extension LMAssembly {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用 reversed 指令讓使用者語彙檔案內的詞條優先順序隨著行數增加而逐漸增高。
|
|
||||||
// 這樣一來就可以在就地新增語彙時徹底複寫優先權。
|
|
||||||
// 將兩句差分也是為了讓 rawUserUnigrams 的類型不受可能的影響。
|
|
||||||
var userPhraseUnigrams = Array(lmUserPhrases.unigramsFor(key: keyChain).reversed())
|
|
||||||
if keyArray.count == 1, let topScore = rawAllUnigrams.map(\.score).max() {
|
|
||||||
// 不再讓使用者自己加入的單漢字讀音權重進入爬軌體系。
|
|
||||||
userPhraseUnigrams = userPhraseUnigrams.map { currentUnigram in
|
|
||||||
Megrez.Unigram(
|
|
||||||
value: currentUnigram.value,
|
|
||||||
score: Swift.min(topScore + 0.000_114_514, currentUnigram.score)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rawAllUnigrams = userPhraseUnigrams + rawAllUnigrams
|
|
||||||
|
|
||||||
// 分析且處理可能存在的 InputToken。
|
// 分析且處理可能存在的 InputToken。
|
||||||
rawAllUnigrams = rawAllUnigrams.map { unigram in
|
rawAllUnigrams = rawAllUnigrams.map { unigram in
|
||||||
let convertedValues = unigram.value.parseAsInputToken(isCHS: isCHS)
|
let convertedValues = unigram.value.parseAsInputToken(isCHS: isCHS)
|
||||||
|
|
|
@ -8,9 +8,9 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Megrez
|
import Megrez
|
||||||
import SwiftExtension
|
import Shared
|
||||||
|
|
||||||
public extension LMAssembly.LMInstantiator {
|
public extension vChewingLM.LMInstantiator {
|
||||||
/// 磁帶模式專用:當前磁帶所規定的花牌鍵。
|
/// 磁帶模式專用:當前磁帶所規定的花牌鍵。
|
||||||
var cassetteWildcardKey: String { Self.lmCassette.wildcardKey }
|
var cassetteWildcardKey: String { Self.lmCassette.wildcardKey }
|
||||||
/// 磁帶模式專用:當前磁帶規定的最大碼長。
|
/// 磁帶模式專用:當前磁帶規定的最大碼長。
|
||||||
|
|
|
@ -11,7 +11,7 @@ import Megrez
|
||||||
|
|
||||||
// MARK: - 日期時間便捷輸入功能
|
// MARK: - 日期時間便捷輸入功能
|
||||||
|
|
||||||
extension LMAssembly.LMInstantiator {
|
extension vChewingLM.LMInstantiator {
|
||||||
func queryDateTimeUnigrams(with key: String = "") -> [Megrez.Unigram] {
|
func queryDateTimeUnigrams(with key: String = "") -> [Megrez.Unigram] {
|
||||||
guard let tokenTrigger = TokenTrigger(rawValue: key) else { return [] }
|
guard let tokenTrigger = TokenTrigger(rawValue: key) else { return [] }
|
||||||
var results = [Megrez.Unigram]()
|
var results = [Megrez.Unigram]()
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import Megrez
|
import Megrez
|
||||||
|
|
||||||
public extension LMAssembly.LMInstantiator {
|
public extension vChewingLM.LMInstantiator {
|
||||||
func supplyNumPadUnigrams(key: String) -> [Megrez.Unigram] {
|
func supplyNumPadUnigrams(key: String) -> [Megrez.Unigram] {
|
||||||
guard let status = config.numPadFWHWStatus else { return [] }
|
guard let status = config.numPadFWHWStatus else { return [] }
|
||||||
let initials = "_NumPad_"
|
let initials = "_NumPad_"
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Megrez
|
import Megrez
|
||||||
|
import Shared
|
||||||
import SQLite3
|
import SQLite3
|
||||||
|
|
||||||
/* ==============
|
/* ==============
|
||||||
|
@ -30,7 +31,6 @@ import SQLite3
|
||||||
) WITHOUT ROWID;
|
) WITHOUT ROWID;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
extension LMAssembly.LMInstantiator {
|
|
||||||
enum CoreColumn: Int32 {
|
enum CoreColumn: Int32 {
|
||||||
case theDataCHS = 1 // 簡體中文
|
case theDataCHS = 1 // 簡體中文
|
||||||
case theDataCHT = 2 // 繁體中文
|
case theDataCHT = 2 // 繁體中文
|
||||||
|
@ -53,26 +53,8 @@ extension LMAssembly.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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
extension vChewingLM.LMInstantiator {
|
||||||
fileprivate static func querySQL(strStmt sqlQuery: String, coreColumn column: CoreColumn, handler: (String) -> Void) {
|
fileprivate static func querySQL(strStmt sqlQuery: String, coreColumn column: CoreColumn, handler: (String) -> Void) {
|
||||||
guard Self.ptrSQL != nil else { return }
|
guard Self.ptrSQL != nil else { return }
|
||||||
performStatementSansResult { ptrStatement in
|
performStatementSansResult { ptrStatement in
|
||||||
|
@ -141,10 +123,9 @@ extension LMAssembly.LMInstantiator {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 根據給定的讀音索引鍵,來獲取原廠標準資料庫辭典內的對應資料陣列的 UTF8 資料、就地分析、生成單元圖陣列。
|
/// 根據給定的讀音索引鍵,來獲取原廠標準資料庫辭典內的對應資料陣列的 UTF8 資料、就地分析、生成單元圖陣列。
|
||||||
/// - Remark: 該函式會無損地返回原廠辭典的結果,不受使用者控頻與資料過濾條件的影響,不包含全字庫的資料。
|
|
||||||
/// - parameters:
|
/// - parameters:
|
||||||
/// - key: 讀音索引鍵。
|
/// - key: 讀音索引鍵。
|
||||||
public func factoryCoreUnigramsFor(key: String) -> [Megrez.Unigram] {
|
func factoryCoreUnigramsFor(key: String) -> [Megrez.Unigram] {
|
||||||
// 此處需要把 ASCII 單引號換成連續兩個單引號,否則會有 SQLite 語句查詢故障。
|
// 此處需要把 ASCII 單引號換成連續兩個單引號,否則會有 SQLite 語句查詢故障。
|
||||||
factoryUnigramsFor(key: key, column: isCHS ? .theDataCHS : .theDataCHT)
|
factoryUnigramsFor(key: key, column: isCHS ? .theDataCHS : .theDataCHT)
|
||||||
}
|
}
|
||||||
|
@ -153,9 +134,7 @@ extension LMAssembly.LMInstantiator {
|
||||||
/// - parameters:
|
/// - parameters:
|
||||||
/// - key: 讀音索引鍵。
|
/// - key: 讀音索引鍵。
|
||||||
/// - column: 資料欄位。
|
/// - column: 資料欄位。
|
||||||
func factoryUnigramsFor(
|
func factoryUnigramsFor(key: String, column: CoreColumn) -> [Megrez.Unigram] {
|
||||||
key: String, column: LMAssembly.LMInstantiator.CoreColumn
|
|
||||||
) -> [Megrez.Unigram] {
|
|
||||||
if key == "_punctuation_list" { return [] }
|
if key == "_punctuation_list" { return [] }
|
||||||
var grams: [Megrez.Unigram] = []
|
var grams: [Megrez.Unigram] = []
|
||||||
var gramsHW: [Megrez.Unigram] = []
|
var gramsHW: [Megrez.Unigram] = []
|
||||||
|
@ -163,10 +142,8 @@ extension LMAssembly.LMInstantiator {
|
||||||
let encryptedKey = Self.cnvPhonabetToASCII(key.replacingOccurrences(of: "'", with: "''"))
|
let encryptedKey = Self.cnvPhonabetToASCII(key.replacingOccurrences(of: "'", with: "''"))
|
||||||
let sqlQuery = "SELECT * FROM DATA_MAIN WHERE theKey='\(encryptedKey)';"
|
let sqlQuery = "SELECT * FROM DATA_MAIN WHERE theKey='\(encryptedKey)';"
|
||||||
Self.querySQL(strStmt: sqlQuery, coreColumn: column) { currentResult in
|
Self.querySQL(strStmt: sqlQuery, coreColumn: column) { currentResult in
|
||||||
var i: Double = 0
|
let arrRangeRecords = currentResult.split(separator: "\t")
|
||||||
var previousScore: Double?
|
for strNetaSet in arrRangeRecords {
|
||||||
currentResult.split(separator: "\t").forEach { strNetaSet in
|
|
||||||
// 這裡假定原廠資料已經經過對權重的 stable sort 排序。
|
|
||||||
let neta = Array(strNetaSet.trimmingCharacters(in: .newlines).split(separator: " ").reversed())
|
let neta = Array(strNetaSet.trimmingCharacters(in: .newlines).split(separator: " ").reversed())
|
||||||
let theValue: String = .init(neta[0])
|
let theValue: String = .init(neta[0])
|
||||||
var theScore = column.defaultScore
|
var theScore = column.defaultScore
|
||||||
|
@ -176,15 +153,8 @@ extension LMAssembly.LMInstantiator {
|
||||||
if theScore > 0 {
|
if theScore > 0 {
|
||||||
theScore *= -1 // 應對可能忘記寫負號的情形
|
theScore *= -1 // 應對可能忘記寫負號的情形
|
||||||
}
|
}
|
||||||
if previousScore == theScore {
|
|
||||||
theScore -= i * 0.000_001
|
|
||||||
i += 1
|
|
||||||
} else {
|
|
||||||
previousScore = theScore
|
|
||||||
i = 0
|
|
||||||
}
|
|
||||||
grams.append(Megrez.Unigram(value: theValue, score: theScore))
|
grams.append(Megrez.Unigram(value: theValue, score: theScore))
|
||||||
if !key.contains("_punctuation") { return }
|
if !key.contains("_punctuation") { continue }
|
||||||
let halfValue = theValue.applyingTransformFW2HW(reverse: false)
|
let halfValue = theValue.applyingTransformFW2HW(reverse: false)
|
||||||
if halfValue != theValue {
|
if halfValue != theValue {
|
||||||
gramsHW.append(Megrez.Unigram(value: halfValue, score: theScore))
|
gramsHW.append(Megrez.Unigram(value: halfValue, score: theScore))
|
||||||
|
@ -195,24 +165,6 @@ extension LMAssembly.LMInstantiator {
|
||||||
return grams
|
return grams
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 根據給定的讀音索引鍵,來獲取原廠 CNS 資料庫辭典內的對應資料陣列的 UTF8 資料。
|
|
||||||
/// 該函式僅用來快速篩查 CNS 檢索結果
|
|
||||||
/// - parameters:
|
|
||||||
/// - key: 讀音索引鍵。
|
|
||||||
/// - column: 資料欄位。
|
|
||||||
private func factoryCNSFilterThreadFor(key: String) -> String? {
|
|
||||||
let column = CoreColumn.theDataCNS
|
|
||||||
if key == "_punctuation_list" { return nil }
|
|
||||||
var results: [String] = []
|
|
||||||
// 此處需要把 ASCII 單引號換成連續兩個單引號,否則會有 SQLite 語句查詢故障。
|
|
||||||
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
|
|
||||||
results.append(currentResult)
|
|
||||||
}
|
|
||||||
return results.joined(separator: "\t")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 根據給定的讀音索引鍵,來獲取原廠資料庫辭典內的對應資料陣列的 UTF8 資料、就地分析、生成單元圖陣列。
|
/// 根據給定的讀音索引鍵,來獲取原廠資料庫辭典內的對應資料陣列的 UTF8 資料、就地分析、生成單元圖陣列。
|
||||||
/// - remark: 該函式暫時用不到,但先不用刪除。沒準今後會有用場。
|
/// - remark: 該函式暫時用不到,但先不用刪除。沒準今後會有用場。
|
||||||
/// - parameters:
|
/// - parameters:
|
||||||
|
@ -225,22 +177,9 @@ extension LMAssembly.LMInstantiator {
|
||||||
let sqlQuery = "SELECT * FROM DATA_MAIN WHERE theKey='\(encryptedKey)' AND \(column.name) IS NOT NULL"
|
let sqlQuery = "SELECT * FROM DATA_MAIN WHERE theKey='\(encryptedKey)' AND \(column.name) IS NOT NULL"
|
||||||
return Self.hasSQLResult(strStmt: sqlQuery)
|
return Self.hasSQLResult(strStmt: sqlQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 檢查該當 Unigram 結果是否完全符合台澎金馬 CNS11643 的規定讀音。
|
|
||||||
/// 該函式不適合拿給簡體中文模式使用。
|
|
||||||
func checkCNSConformation(for unigram: Megrez.Unigram, keyArray: [String]) -> Bool {
|
|
||||||
guard unigram.value.count == keyArray.count else { return true }
|
|
||||||
let chars = unigram.value.map(\.description)
|
|
||||||
for (i, key) in keyArray.enumerated() {
|
|
||||||
guard !key.hasPrefix("_") else { continue }
|
|
||||||
guard let matchedCNSResult = factoryCNSFilterThreadFor(key: key) else { continue }
|
|
||||||
guard matchedCNSResult.contains(chars[i]) else { return false }
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension LMAssembly.LMInstantiator {
|
private extension vChewingLM.LMInstantiator {
|
||||||
/// 內部函式,用以將注音讀音索引鍵進行加密。
|
/// 內部函式,用以將注音讀音索引鍵進行加密。
|
||||||
///
|
///
|
||||||
/// 使用這種加密字串作為索引鍵,可以增加對 json 資料庫的存取速度。
|
/// 使用這種加密字串作為索引鍵,可以增加對 json 資料庫的存取速度。
|
||||||
|
@ -288,7 +227,7 @@ private extension LMAssembly.LMInstantiator {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension LMAssembly.LMInstantiator {
|
public extension vChewingLM.LMInstantiator {
|
||||||
@discardableResult static func connectToTestSQLDB() -> Bool {
|
@discardableResult static func connectToTestSQLDB() -> Bool {
|
||||||
Self.connectSQLDB(dbPath: #":memory:"#) && sqlTestCoreLMData.runAsSQLExec(dbPointer: &ptrSQL)
|
Self.connectSQLDB(dbPath: #":memory:"#) && sqlTestCoreLMData.runAsSQLExec(dbPointer: &ptrSQL)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,60 +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 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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,33 +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
|
|
||||||
|
|
||||||
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 : [:]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,123 +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
|
|
||||||
|
|
||||||
// 該檔案使得 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":"ㄓㄨㄤ"}
|
|
||||||
}
|
|
||||||
"""#
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -7,11 +7,13 @@
|
||||||
// requirements defined in MIT License.
|
// requirements defined in MIT License.
|
||||||
|
|
||||||
import Megrez
|
import Megrez
|
||||||
|
import PinyinPhonaConverter
|
||||||
|
import Shared
|
||||||
|
|
||||||
extension LMAssembly {
|
public extension vChewingLM {
|
||||||
struct LMAssociates {
|
@frozen struct LMAssociates {
|
||||||
public private(set) var filePath: String?
|
public private(set) var filePath: String?
|
||||||
var rangeMap: [String: [(Range<String.Index>, Int)]] = [:] // Range 只可能是一整行,所以必須得有 index。
|
var rangeMap: [String: [(Range<String.Index>, Int)]] = [:]
|
||||||
var strData: String = ""
|
var strData: String = ""
|
||||||
|
|
||||||
public var count: Int { rangeMap.count }
|
public var count: Int { rangeMap.count }
|
||||||
|
@ -46,8 +48,8 @@ extension LMAssembly {
|
||||||
replaceData(textData: rawStrData)
|
replaceData(textData: rawStrData)
|
||||||
} catch {
|
} catch {
|
||||||
filePath = oldPath
|
filePath = oldPath
|
||||||
vCLMLog("\(error)")
|
vCLog("\(error)")
|
||||||
vCLMLog("↑ Exception happened when reading data at: \(path).")
|
vCLog("↑ Exception happened when reading data at: \(path).")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,21 +93,28 @@ extension LMAssembly {
|
||||||
do {
|
do {
|
||||||
try strData.write(toFile: filePath, atomically: true, encoding: .utf8)
|
try strData.write(toFile: filePath, atomically: true, encoding: .utf8)
|
||||||
} catch {
|
} catch {
|
||||||
vCLMLog("Failed to save current database to: \(filePath)")
|
vCLog("Failed to save current database to: \(filePath)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func valuesFor(pair: Megrez.KeyValuePaired) -> [String] {
|
public func valuesFor(pair: Megrez.KeyValuePaired) -> [String] {
|
||||||
var pairs: [String] = []
|
var pairs: [String] = []
|
||||||
let availableResults = [rangeMap[pair.toNGramKey], rangeMap[pair.value]].compactMap { $0 }
|
if let arrRangeRecords: [(Range<String.Index>, Int)] = rangeMap[pair.toNGramKey] {
|
||||||
availableResults.forEach { arrRangeRecords in
|
for (netaRange, index) in arrRangeRecords {
|
||||||
arrRangeRecords.forEach { netaRange, index in
|
|
||||||
let neta = strData[netaRange].split(separator: " ")
|
let neta = strData[netaRange].split(separator: " ")
|
||||||
let theValue: String = .init(neta[index])
|
let theValue: String = .init(neta[index])
|
||||||
pairs.append(theValue)
|
pairs.append(theValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return pairs.deduplicated
|
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 }
|
||||||
}
|
}
|
||||||
|
|
||||||
public func hasValuesFor(pair: Megrez.KeyValuePaired) -> Bool {
|
public func hasValuesFor(pair: Megrez.KeyValuePaired) -> Bool {
|
||||||
|
@ -114,17 +123,3 @@ extension LMAssembly {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -10,10 +10,11 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import LineReader
|
import LineReader
|
||||||
import Megrez
|
import Megrez
|
||||||
|
import Shared
|
||||||
|
|
||||||
extension LMAssembly {
|
public extension vChewingLM {
|
||||||
/// 磁帶模組,用來方便使用者自行擴充字根輸入法。
|
/// 磁帶模組,用來方便使用者自行擴充字根輸入法。
|
||||||
struct LMCassette {
|
@frozen struct LMCassette {
|
||||||
public private(set) var filePath: String?
|
public private(set) var filePath: String?
|
||||||
public private(set) var nameShort: String = ""
|
public private(set) var nameShort: String = ""
|
||||||
public private(set) var nameENG: String = ""
|
public private(set) var nameENG: String = ""
|
||||||
|
@ -39,13 +40,12 @@ extension LMAssembly {
|
||||||
public private(set) var areCandidateKeysShiftHeld: Bool = false
|
public private(set) var areCandidateKeysShiftHeld: Bool = false
|
||||||
public private(set) var supplyQuickResults: Bool = false
|
public private(set) var supplyQuickResults: Bool = false
|
||||||
public private(set) var supplyPartiallyMatchedResults: Bool = false
|
public private(set) var supplyPartiallyMatchedResults: Bool = false
|
||||||
public var candidateKeysValidator: (String) -> Bool = { _ in false }
|
|
||||||
/// 計算頻率時要用到的東西 - NORM
|
/// 計算頻率時要用到的東西 - NORM
|
||||||
private var norm = 0.0
|
private var norm = 0.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LMAssembly.LMCassette {
|
public extension vChewingLM.LMCassette {
|
||||||
/// 計算頻率時要用到的東西 - fscale
|
/// 計算頻率時要用到的東西 - fscale
|
||||||
private static let fscale = 2.7
|
private static let fscale = 2.7
|
||||||
/// 萬用花牌字符,哪怕花牌鍵仍不可用。
|
/// 萬用花牌字符,哪怕花牌鍵仍不可用。
|
||||||
|
@ -86,7 +86,7 @@ extension LMAssembly.LMCassette {
|
||||||
if FileManager.default.fileExists(atPath: path) {
|
if FileManager.default.fileExists(atPath: path) {
|
||||||
do {
|
do {
|
||||||
guard let fileHandle = FileHandle(forReadingAtPath: path) else {
|
guard let fileHandle = FileHandle(forReadingAtPath: path) else {
|
||||||
throw LMAssembly.FileErrors.fileHandleError("")
|
throw vChewingLM.FileErrors.fileHandleError("")
|
||||||
}
|
}
|
||||||
let lineReader = try LineReader(file: fileHandle)
|
let lineReader = try LineReader(file: fileHandle)
|
||||||
var theMaxKeyLength = 1
|
var theMaxKeyLength = 1
|
||||||
|
@ -193,9 +193,7 @@ extension LMAssembly.LMCassette {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Post process.
|
// 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 {
|
if !keysUsedInCharDef.intersection(selectionKeys.map(\.description)).isEmpty {
|
||||||
areCandidateKeysShiftHeld = true
|
areCandidateKeysShiftHeld = true
|
||||||
}
|
}
|
||||||
|
@ -204,10 +202,10 @@ extension LMAssembly.LMCassette {
|
||||||
filePath = path
|
filePath = path
|
||||||
return true
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
vCLMLog("CIN Loading Failed: File Access Error.")
|
vCLog("CIN Loading Failed: File Access Error.")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
vCLMLog("CIN Loading Failed: File Missing.")
|
vCLog("CIN Loading Failed: File Missing.")
|
||||||
}
|
}
|
||||||
filePath = oldPath
|
filePath = oldPath
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -7,13 +7,15 @@
|
||||||
// requirements defined in MIT License.
|
// requirements defined in MIT License.
|
||||||
|
|
||||||
import Megrez
|
import Megrez
|
||||||
|
import PinyinPhonaConverter
|
||||||
|
import Shared
|
||||||
|
|
||||||
extension LMAssembly {
|
public extension vChewingLM {
|
||||||
/// 與之前的 LMCore 不同,LMCoreEX 不在辭典內記錄實體,而是記錄 range 範圍。
|
/// 與之前的 LMCore 不同,LMCoreEX 不在辭典內記錄實體,而是記錄 range 範圍。
|
||||||
/// 需要資料的時候,直接拿 range 去 strData 取資料。
|
/// 需要資料的時候,直接拿 range 去 strData 取資料。
|
||||||
/// 資料記錄原理與上游 C++ 的 ParselessLM 差不多,但用的是 Swift 原生手段。
|
/// 資料記錄原理與上游 C++ 的 ParselessLM 差不多,但用的是 Swift 原生手段。
|
||||||
/// 主要時間消耗仍在 For 迴圈,但這個算法可以顯著減少記憶體佔用。
|
/// 主要時間消耗仍在 For 迴圈,但這個算法可以顯著減少記憶體佔用。
|
||||||
struct LMCoreEX {
|
@frozen struct LMCoreEX {
|
||||||
public private(set) var filePath: String?
|
public private(set) var filePath: String?
|
||||||
/// 資料庫辭典。索引內容為注音字串,資料內容則為字串首尾範圍、方便自 strData 取資料。
|
/// 資料庫辭典。索引內容為注音字串,資料內容則為字串首尾範圍、方便自 strData 取資料。
|
||||||
var rangeMap: [String: [Range<String.Index>]] = [:]
|
var rangeMap: [String: [Range<String.Index>]] = [:]
|
||||||
|
@ -79,8 +81,8 @@ extension LMAssembly {
|
||||||
replaceData(textData: rawStrData)
|
replaceData(textData: rawStrData)
|
||||||
} catch {
|
} catch {
|
||||||
filePath = oldPath
|
filePath = oldPath
|
||||||
vCLMLog("\(error)")
|
vCLog("\(error)")
|
||||||
vCLMLog("↑ Exception happened when reading data at: \(path).")
|
vCLog("↑ Exception happened when reading data at: \(path).")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,7 +133,7 @@ extension LMAssembly {
|
||||||
}
|
}
|
||||||
try dataToWrite.write(toFile: filePath, atomically: true, encoding: .utf8)
|
try dataToWrite.write(toFile: filePath, atomically: true, encoding: .utf8)
|
||||||
} catch {
|
} catch {
|
||||||
vCLMLog("Failed to save current database to: \(filePath)")
|
vCLog("Failed to save current database to: \(filePath)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,7 +150,7 @@ extension LMAssembly {
|
||||||
strDump += addline
|
strDump += addline
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
vCLMLog(strDump)
|
vCLog(strDump)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 根據給定的讀音索引鍵,來獲取資料庫辭典內的對應資料陣列的字串首尾範圍資料、據此自 strData 取得字串形式的資料、生成單元圖陣列。
|
/// 根據給定的讀音索引鍵,來獲取資料庫辭典內的對應資料陣列的字串首尾範圍資料、據此自 strData 取得字串形式的資料、生成單元圖陣列。
|
||||||
|
@ -184,15 +186,3 @@ extension LMAssembly {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -7,36 +7,67 @@
|
||||||
// requirements defined in MIT License.
|
// requirements defined in MIT License.
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Shared
|
||||||
|
|
||||||
extension LMAssembly {
|
public extension vChewingLM {
|
||||||
struct LMPlainBopomofo {
|
@frozen struct LMPlainBopomofo {
|
||||||
@usableFromInline typealias DataMap = [String: [String: String]]
|
public private(set) var filePath: String?
|
||||||
let dataMap: DataMap
|
var dataMap: [String: String] = [:]
|
||||||
|
|
||||||
public var count: Int { dataMap.count }
|
public var count: Int { dataMap.count }
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
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 = [:]
|
dataMap = [:]
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public var isLoaded: Bool { !dataMap.isEmpty }
|
public var isLoaded: Bool { !dataMap.isEmpty }
|
||||||
|
|
||||||
public func valuesFor(key: String, isCHS: Bool) -> [String] {
|
@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] {
|
||||||
var pairs: [String] = []
|
var pairs: [String] = []
|
||||||
let subKey = isCHS ? "S" : "T"
|
if let arrRangeRecords: String = dataMap[key]?.trimmingCharacters(in: .newlines) {
|
||||||
if let arrRangeRecords: String = dataMap[key]?[subKey] {
|
|
||||||
pairs.append(contentsOf: arrRangeRecords.map(\.description))
|
pairs.append(contentsOf: arrRangeRecords.map(\.description))
|
||||||
}
|
}
|
||||||
// 這裡不做去重複處理,因為倚天中文系統注音排序適應者們已經形成了肌肉記憶。
|
return pairs.deduplicated
|
||||||
return pairs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func hasValuesFor(key: String) -> Bool { dataMap.keys.contains(key) }
|
public func hasValuesFor(key: String) -> Bool { dataMap.keys.contains(key) }
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -6,8 +6,10 @@
|
||||||
// marks, or product names of Contributor, except as required to fulfill notice
|
// marks, or product names of Contributor, except as required to fulfill notice
|
||||||
// requirements defined in MIT License.
|
// requirements defined in MIT License.
|
||||||
|
|
||||||
extension LMAssembly {
|
import Shared
|
||||||
struct LMReplacements {
|
|
||||||
|
public extension vChewingLM {
|
||||||
|
@frozen struct LMReplacements {
|
||||||
public private(set) var filePath: String?
|
public private(set) var filePath: String?
|
||||||
var rangeMap: [String: Range<String.Index>] = [:]
|
var rangeMap: [String: Range<String.Index>] = [:]
|
||||||
var strData: String = ""
|
var strData: String = ""
|
||||||
|
@ -33,8 +35,8 @@ extension LMAssembly {
|
||||||
replaceData(textData: rawStrData)
|
replaceData(textData: rawStrData)
|
||||||
} catch {
|
} catch {
|
||||||
filePath = oldPath
|
filePath = oldPath
|
||||||
vCLMLog("\(error)")
|
vCLog("\(error)")
|
||||||
vCLMLog("↑ Exception happened when reading data at: \(path).")
|
vCLog("↑ Exception happened when reading data at: \(path).")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,7 +72,7 @@ extension LMAssembly {
|
||||||
do {
|
do {
|
||||||
try strData.write(toFile: filePath, atomically: true, encoding: .utf8)
|
try strData.write(toFile: filePath, atomically: true, encoding: .utf8)
|
||||||
} catch {
|
} catch {
|
||||||
vCLMLog("Failed to save current database to: \(filePath)")
|
vCLog("Failed to save current database to: \(filePath)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +81,7 @@ extension LMAssembly {
|
||||||
for entry in rangeMap {
|
for entry in rangeMap {
|
||||||
strDump += strData[entry.value] + "\n"
|
strDump += strData[entry.value] + "\n"
|
||||||
}
|
}
|
||||||
vCLMLog(strDump)
|
vCLog(strDump)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func valuesFor(key: String) -> String {
|
public func valuesFor(key: String) -> String {
|
||||||
|
@ -98,13 +100,3 @@ extension LMAssembly {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LMAssembly.LMReplacements {
|
|
||||||
var dictRepresented: [String: String] {
|
|
||||||
var result = [String: String]()
|
|
||||||
rangeMap.forEach { key, valueRange in
|
|
||||||
result[key] = strData[valueRange].description
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
// (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": "˙",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,41 +9,74 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Megrez
|
import Megrez
|
||||||
|
import Shared
|
||||||
|
|
||||||
// MARK: - Public Types.
|
public extension vChewingLM {
|
||||||
|
|
||||||
public extension LMAssembly {
|
|
||||||
struct OverrideSuggestion {
|
|
||||||
public var candidates = [(String, Megrez.Unigram)]()
|
|
||||||
public var forceHighScoreOverride = false
|
|
||||||
public var isEmpty: Bool { candidates.isEmpty }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - LMUserOverride Class Definition.
|
|
||||||
|
|
||||||
extension LMAssembly {
|
|
||||||
class LMUserOverride {
|
class LMUserOverride {
|
||||||
|
// MARK: - Main
|
||||||
|
|
||||||
var mutCapacity: Int
|
var mutCapacity: Int
|
||||||
var mutDecayExponent: Double
|
var mutDecayExponent: Double
|
||||||
var mutLRUList: [KeyObservationPair] = []
|
var mutLRUList: [KeyObservationPair] = []
|
||||||
var mutLRUMap: [String: KeyObservationPair] = [:]
|
var mutLRUMap: [String: KeyObservationPair] = [:]
|
||||||
let kDecayThreshold: Double = 1.0 / 1_048_576.0 // 衰減二十次之後差不多就失效了。
|
let kDecayThreshold: Double = 1.0 / 1_048_576.0 // 衰減二十次之後差不多就失效了。
|
||||||
var fileSaveLocationURL: URL?
|
var fileSaveLocationURL: URL
|
||||||
|
|
||||||
public static let kObservedOverrideHalfLife: Double = 3600.0 * 6 // 6 小時半衰一次,能持續不到六天的記憶。
|
public static let kObservedOverrideHalfLife: Double = 3600.0 * 6 // 6 小時半衰一次,能持續不到六天的記憶。
|
||||||
|
|
||||||
public init(capacity: Int = 500, decayConstant: Double = LMUserOverride.kObservedOverrideHalfLife, dataURL: URL? = nil) {
|
public init(capacity: Int = 500, decayConstant: Double = LMUserOverride.kObservedOverrideHalfLife, dataURL: URL) {
|
||||||
mutCapacity = max(capacity, 1) // Ensures that this integer value is always > 0.
|
mutCapacity = max(capacity, 1) // Ensures that this integer value is always > 0.
|
||||||
mutDecayExponent = log(0.5) / decayConstant
|
mutDecayExponent = log(0.5) / decayConstant
|
||||||
fileSaveLocationURL = dataURL
|
fileSaveLocationURL = dataURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func performObservation(
|
||||||
|
walkedBefore: [Megrez.Node], walkedAfter: [Megrez.Node],
|
||||||
|
cursor: Int, timestamp: Double, saveCallback: @escaping () -> Void
|
||||||
|
) {
|
||||||
|
// 參數合規性檢查。
|
||||||
|
guard !walkedAfter.isEmpty, !walkedBefore.isEmpty else { return }
|
||||||
|
guard walkedBefore.totalKeyCount == walkedAfter.totalKeyCount else { return }
|
||||||
|
// 先判斷用哪種覆寫方法。
|
||||||
|
var actualCursor = 0
|
||||||
|
guard let currentNode = walkedAfter.findNode(at: cursor, target: &actualCursor) else { return }
|
||||||
|
// 當前節點超過三個字的話,就不記憶了。在這種情形下,使用者可以考慮新增自訂語彙。
|
||||||
|
guard currentNode.spanLength <= 3 else { return }
|
||||||
|
// 前一個節點得從前一次爬軌結果當中來找。
|
||||||
|
guard actualCursor > 0 else { return } // 該情況應該不會出現。
|
||||||
|
let currentNodeIndex = actualCursor
|
||||||
|
actualCursor -= 1
|
||||||
|
var prevNodeIndex = 0
|
||||||
|
guard let prevNode = walkedBefore.findNode(at: actualCursor, target: &prevNodeIndex) else { return }
|
||||||
|
|
||||||
|
let forceHighScoreOverride: Bool = currentNode.spanLength > prevNode.spanLength
|
||||||
|
let breakingUp = currentNode.spanLength == 1 && prevNode.spanLength > 1
|
||||||
|
|
||||||
|
let targetNodeIndex = breakingUp ? currentNodeIndex : prevNodeIndex
|
||||||
|
let key: String = vChewingLM.LMUserOverride.formObservationKey(
|
||||||
|
walkedNodes: walkedAfter, headIndex: targetNodeIndex
|
||||||
|
)
|
||||||
|
guard !key.isEmpty else { return }
|
||||||
|
doObservation(
|
||||||
|
key: key, candidate: currentNode.currentUnigram.value, timestamp: timestamp,
|
||||||
|
forceHighScoreOverride: forceHighScoreOverride, saveCallback: { saveCallback() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func fetchSuggestion(
|
||||||
|
currentWalk: [Megrez.Node], cursor: Int, timestamp: Double
|
||||||
|
) -> Suggestion {
|
||||||
|
var headIndex = 0
|
||||||
|
guard let nodeIter = currentWalk.findNode(at: cursor, target: &headIndex) else { return .init() }
|
||||||
|
let key = vChewingLM.LMUserOverride.formObservationKey(walkedNodes: currentWalk, headIndex: headIndex)
|
||||||
|
return getSuggestion(key: key, timestamp: timestamp, headReading: nodeIter.joinedKey())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private Structures
|
// MARK: - Private Structures
|
||||||
|
|
||||||
extension LMAssembly.LMUserOverride {
|
extension vChewingLM.LMUserOverride {
|
||||||
enum OverrideUnit: CodingKey { case count, timestamp, forceHighScoreOverride }
|
enum OverrideUnit: CodingKey { case count, timestamp, forceHighScoreOverride }
|
||||||
enum ObservationUnit: CodingKey { case count, overrides }
|
enum ObservationUnit: CodingKey { case count, overrides }
|
||||||
enum KeyObservationPairUnit: CodingKey { case key, observation }
|
enum KeyObservationPairUnit: CodingKey { case key, observation }
|
||||||
|
@ -120,52 +153,10 @@ extension LMAssembly.LMUserOverride {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Internal Methods in LMAssembly.
|
// MARK: - Hash and Dehash the entire UOM data, etc.
|
||||||
|
|
||||||
extension LMAssembly.LMUserOverride {
|
public extension vChewingLM.LMUserOverride {
|
||||||
func performObservation(
|
func bleachSpecifiedSuggestions(targets: [String], saveCallback: @escaping () -> Void) {
|
||||||
walkedBefore: [Megrez.Node], walkedAfter: [Megrez.Node],
|
|
||||||
cursor: Int, timestamp: Double, saveCallback: (() -> Void)? = nil
|
|
||||||
) {
|
|
||||||
// 參數合規性檢查。
|
|
||||||
guard !walkedAfter.isEmpty, !walkedBefore.isEmpty else { return }
|
|
||||||
guard walkedBefore.totalKeyCount == walkedAfter.totalKeyCount else { return }
|
|
||||||
// 先判斷用哪種覆寫方法。
|
|
||||||
var actualCursor = 0
|
|
||||||
guard let currentNode = walkedAfter.findNode(at: cursor, target: &actualCursor) else { return }
|
|
||||||
// 當前節點超過三個字的話,就不記憶了。在這種情形下,使用者可以考慮新增自訂語彙。
|
|
||||||
guard currentNode.spanLength <= 3 else { return }
|
|
||||||
// 前一個節點得從前一次爬軌結果當中來找。
|
|
||||||
guard actualCursor > 0 else { return } // 該情況應該不會出現。
|
|
||||||
let currentNodeIndex = actualCursor
|
|
||||||
actualCursor -= 1
|
|
||||||
var prevNodeIndex = 0
|
|
||||||
guard let prevNode = walkedBefore.findNode(at: actualCursor, target: &prevNodeIndex) else { return }
|
|
||||||
|
|
||||||
let forceHighScoreOverride: Bool = currentNode.spanLength > prevNode.spanLength
|
|
||||||
let breakingUp = currentNode.spanLength == 1 && prevNode.spanLength > 1
|
|
||||||
|
|
||||||
let targetNodeIndex = breakingUp ? currentNodeIndex : prevNodeIndex
|
|
||||||
let key: String = LMAssembly.LMUserOverride.formObservationKey(
|
|
||||||
walkedNodes: walkedAfter, headIndex: targetNodeIndex
|
|
||||||
)
|
|
||||||
guard !key.isEmpty else { return }
|
|
||||||
doObservation(
|
|
||||||
key: key, candidate: currentNode.currentUnigram.value, timestamp: timestamp,
|
|
||||||
forceHighScoreOverride: forceHighScoreOverride, saveCallback: saveCallback
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchSuggestion(
|
|
||||||
currentWalk: [Megrez.Node], cursor: Int, timestamp: Double
|
|
||||||
) -> LMAssembly.OverrideSuggestion {
|
|
||||||
var headIndex = 0
|
|
||||||
guard let nodeIter = currentWalk.findNode(at: cursor, target: &headIndex) else { return .init() }
|
|
||||||
let key = LMAssembly.LMUserOverride.formObservationKey(walkedNodes: currentWalk, headIndex: headIndex)
|
|
||||||
return getSuggestion(key: key, timestamp: timestamp, headReading: nodeIter.joinedKey())
|
|
||||||
}
|
|
||||||
|
|
||||||
func bleachSpecifiedSuggestions(targets: [String], saveCallback: (() -> Void)? = nil) {
|
|
||||||
if targets.isEmpty { return }
|
if targets.isEmpty { return }
|
||||||
for neta in mutLRUMap {
|
for neta in mutLRUMap {
|
||||||
for target in targets {
|
for target in targets {
|
||||||
|
@ -175,86 +166,82 @@ extension LMAssembly.LMUserOverride {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resetMRUList()
|
resetMRUList()
|
||||||
saveCallback?() ?? saveData()
|
saveCallback()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 自 LRU 辭典內移除所有的單元圖。
|
/// 自 LRU 辭典內移除所有的單元圖。
|
||||||
func bleachUnigrams(saveCallback: (() -> Void)? = nil) {
|
func bleachUnigrams(saveCallback: @escaping () -> Void) {
|
||||||
for key in mutLRUMap.keys {
|
for key in mutLRUMap.keys {
|
||||||
if !key.contains("(),()") { continue }
|
if !key.contains("(),()") { continue }
|
||||||
mutLRUMap.removeValue(forKey: key)
|
mutLRUMap.removeValue(forKey: key)
|
||||||
}
|
}
|
||||||
resetMRUList()
|
resetMRUList()
|
||||||
saveCallback?() ?? saveData()
|
saveCallback()
|
||||||
}
|
}
|
||||||
|
|
||||||
func resetMRUList() {
|
internal func resetMRUList() {
|
||||||
mutLRUList.removeAll()
|
mutLRUList.removeAll()
|
||||||
for neta in mutLRUMap.reversed() {
|
for neta in mutLRUMap.reversed() {
|
||||||
mutLRUList.append(neta.value)
|
mutLRUList.append(neta.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func clearData(withURL fileURL: URL? = nil) {
|
func clearData(withURL fileURL: URL) {
|
||||||
mutLRUMap = .init()
|
mutLRUMap = .init()
|
||||||
mutLRUList = .init()
|
mutLRUList = .init()
|
||||||
do {
|
do {
|
||||||
let nullData = "{}"
|
let nullData = "{}"
|
||||||
guard let fileURL = fileURL ?? fileSaveLocationURL else {
|
|
||||||
throw UOMError(rawValue: "given fileURL is invalid or nil.")
|
|
||||||
}
|
|
||||||
try nullData.write(to: fileURL, atomically: false, encoding: .utf8)
|
try nullData.write(to: fileURL, atomically: false, encoding: .utf8)
|
||||||
} catch {
|
} catch {
|
||||||
vCLMLog("UOM Error: Unable to clear the data in the UOM file. Details: \(error)")
|
vCLog("UOM Error: Unable to clear data. Details: \(error)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveData(toURL fileURL: URL? = nil) {
|
func saveData(toURL fileURL: URL? = nil) {
|
||||||
guard let fileURL: URL = fileURL ?? fileSaveLocationURL else {
|
|
||||||
vCLMLog("UOM saveData() failed. At least the file Save URL is not set for the current UOM.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 此處不要使用 JSONSerialization,不然執行緒會炸掉。
|
// 此處不要使用 JSONSerialization,不然執行緒會炸掉。
|
||||||
let encoder = JSONEncoder()
|
let encoder = JSONEncoder()
|
||||||
do {
|
do {
|
||||||
guard let jsonData = try? encoder.encode(mutLRUMap) else { return }
|
guard let jsonData = try? encoder.encode(mutLRUMap) else { return }
|
||||||
|
let fileURL: URL = fileURL ?? fileSaveLocationURL
|
||||||
try jsonData.write(to: fileURL, options: .atomic)
|
try jsonData.write(to: fileURL, options: .atomic)
|
||||||
} catch {
|
} catch {
|
||||||
vCLMLog("UOM Error: Unable to save data, abort saving. Details: \(error)")
|
vCLog("UOM Error: Unable to save data, abort saving. Details: \(error)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadData(fromURL fileURL: URL? = nil) {
|
func loadData(fromURL fileURL: URL) {
|
||||||
guard let fileURL: URL = fileURL ?? fileSaveLocationURL else {
|
|
||||||
vCLMLog("UOM loadData() failed. At least the file Load URL is not set for the current UOM.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 此處不要使用 JSONSerialization,不然執行緒會炸掉。
|
// 此處不要使用 JSONSerialization,不然執行緒會炸掉。
|
||||||
let decoder = JSONDecoder()
|
let decoder = JSONDecoder()
|
||||||
do {
|
do {
|
||||||
let data = try Data(contentsOf: fileURL, options: .mappedIfSafe)
|
let data = try Data(contentsOf: fileURL, options: .mappedIfSafe)
|
||||||
if ["", "{}"].contains(String(data: data, encoding: .utf8)) { return }
|
if ["", "{}"].contains(String(data: data, encoding: .utf8)) { return }
|
||||||
guard let jsonResult = try? decoder.decode([String: KeyObservationPair].self, from: data) else {
|
guard let jsonResult = try? decoder.decode([String: KeyObservationPair].self, from: data) else {
|
||||||
vCLMLog("UOM Error: Read file content type invalid, abort loading.")
|
vCLog("UOM Error: Read file content type invalid, abort loading.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
mutLRUMap = jsonResult
|
mutLRUMap = jsonResult
|
||||||
resetMRUList()
|
resetMRUList()
|
||||||
} catch {
|
} catch {
|
||||||
vCLMLog("UOM Error: Unable to read file or parse the data, abort loading. Details: \(error)")
|
vCLog("UOM Error: Unable to read file or parse the data, abort loading. Details: \(error)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct Suggestion {
|
||||||
|
public var candidates = [(String, Megrez.Unigram)]()
|
||||||
|
public var forceHighScoreOverride = false
|
||||||
|
public var isEmpty: Bool { candidates.isEmpty }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Other Non-Public Internal Methods
|
// MARK: - Private Methods
|
||||||
|
|
||||||
extension LMAssembly.LMUserOverride {
|
extension vChewingLM.LMUserOverride {
|
||||||
func doObservation(
|
func doObservation(
|
||||||
key: String, candidate: String, timestamp: Double, forceHighScoreOverride: Bool,
|
key: String, candidate: String, timestamp: Double, forceHighScoreOverride: Bool,
|
||||||
saveCallback: (() -> Void)?
|
saveCallback: @escaping () -> Void
|
||||||
) {
|
) {
|
||||||
guard mutLRUMap[key] != nil else {
|
guard mutLRUMap[key] != nil else {
|
||||||
var observation: Observation = .init()
|
var observation: Observation = .init()
|
||||||
|
@ -270,8 +257,8 @@ extension LMAssembly.LMUserOverride {
|
||||||
mutLRUMap.removeValue(forKey: mutLRUList[mutLRUList.endIndex - 1].key)
|
mutLRUMap.removeValue(forKey: mutLRUList[mutLRUList.endIndex - 1].key)
|
||||||
mutLRUList.removeLast()
|
mutLRUList.removeLast()
|
||||||
}
|
}
|
||||||
vCLMLog("UOM: Observation finished with new observation: \(key)")
|
vCLog("UOM: Observation finished with new observation: \(key)")
|
||||||
saveCallback?() ?? saveData()
|
saveCallback()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 這裡還是不要做 decayCallback 判定「是否不急著更新觀察」了,不然會在嘗試覆寫掉錯誤的記憶時失敗。
|
// 這裡還是不要做 decayCallback 判定「是否不急著更新觀察」了,不然會在嘗試覆寫掉錯誤的記憶時失敗。
|
||||||
|
@ -281,12 +268,12 @@ extension LMAssembly.LMUserOverride {
|
||||||
)
|
)
|
||||||
mutLRUList.insert(theNeta, at: 0)
|
mutLRUList.insert(theNeta, at: 0)
|
||||||
mutLRUMap[key] = theNeta
|
mutLRUMap[key] = theNeta
|
||||||
vCLMLog("UOM: Observation finished with existing observation: \(key)")
|
vCLog("UOM: Observation finished with existing observation: \(key)")
|
||||||
saveCallback?() ?? saveData()
|
saveCallback()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSuggestion(key: String, timestamp: Double, headReading: String) -> LMAssembly.OverrideSuggestion {
|
func getSuggestion(key: String, timestamp: Double, headReading: String) -> Suggestion {
|
||||||
guard !key.isEmpty, let kvPair = mutLRUMap[key] else { return .init() }
|
guard !key.isEmpty, let kvPair = mutLRUMap[key] else { return .init() }
|
||||||
let observation: Observation = kvPair.observation
|
let observation: Observation = kvPair.observation
|
||||||
var candidates: [(String, Megrez.Unigram)] = .init()
|
var candidates: [(String, Megrez.Unigram)] = .init()
|
||||||
|
@ -399,10 +386,3 @@ extension LMAssembly.LMUserOverride {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct UOMError: LocalizedError {
|
|
||||||
var rawValue: String
|
|
||||||
var errorDescription: String? {
|
|
||||||
NSLocalizedString("rawValue", comment: "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
// 下述詞頻資料取自 libTaBE 資料庫 (http://sourceforge.net/projects/libtabe/)
|
//
|
||||||
// (2002 最終版). 該專案於 1999 年由 Pai-Hsiang Hsiao 發起、以 BSD 授權發行。
|
// File.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by ShikiSuen on 2023/11/26.
|
||||||
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
@ -25,8 +29,6 @@ INSERT INTO DATA_MAIN VALUES('de5','-3.516024 的\t-7.427179 得','-3.516024 的
|
||||||
INSERT INTO DATA_MAIN VALUES('di2','-3.516024 的','-3.516024 的',NULL,NULL,NULL,NULL);
|
INSERT INTO DATA_MAIN VALUES('di2','-3.516024 的','-3.516024 的',NULL,NULL,NULL,NULL);
|
||||||
INSERT INTO DATA_MAIN VALUES('di4','-3.516024 的','-3.516024 的',NULL,NULL,NULL,NULL);
|
INSERT INTO DATA_MAIN VALUES('di4','-3.516024 的','-3.516024 的',NULL,NULL,NULL,NULL);
|
||||||
INSERT INTO DATA_MAIN VALUES('duP3','-9.544 㨃','-9.544 㨃','㨃\t䇏\t𦞙\t謉\t𠡒\t𡑈\t𥫉\t𦞱\t𧫏\t𩛔','','','');
|
INSERT INTO DATA_MAIN VALUES('duP3','-9.544 㨃','-9.544 㨃','㨃\t䇏\t𦞙\t謉\t𠡒\t𡑈\t𥫉\t𦞱\t𧫏\t𩛔','','','');
|
||||||
INSERT INTO DATA_MAIN VALUES('uP','-6.0 危','-6.0 危',NULL,NULL,NULL,NULL); /* 用來測試 CNS 過濾器的。 */
|
|
||||||
INSERT INTO DATA_MAIN VALUES('uP2','-6.0 危','-6.0 危','-6.0 危',NULL,NULL,NULL); /* 用來測試 CNS 過濾器的。 */
|
|
||||||
INSERT INTO DATA_MAIN VALUES('fL','-11.0 🐝','-11.0 🐝',NULL,NULL,NULL,NULL);
|
INSERT INTO DATA_MAIN VALUES('fL','-11.0 🐝','-11.0 🐝',NULL,NULL,NULL,NULL);
|
||||||
INSERT INTO DATA_MAIN VALUES('gM','-7.171551 高\t-11.92872 膏\t-13.624335 篙\t-12.390804 糕','-7.171551 高\t-11.92872 膏\t-13.624335 篙\t-12.390804 糕',NULL,NULL,NULL,NULL);
|
INSERT INTO DATA_MAIN VALUES('gM','-7.171551 高\t-11.92872 膏\t-13.624335 篙\t-12.390804 糕','-7.171551 高\t-11.92872 膏\t-13.624335 篙\t-12.390804 糕',NULL,NULL,NULL,NULL);
|
||||||
INSERT INTO DATA_MAIN VALUES('gM-ke-ji4','-9.842421 高科技','-9.842421 高科技',NULL,NULL,NULL,NULL);
|
INSERT INTO DATA_MAIN VALUES('gM-ke-ji4','-9.842421 高科技','-9.842421 高科技',NULL,NULL,NULL,NULL);
|
||||||
|
|
|
@ -7,9 +7,10 @@
|
||||||
// requirements defined in MIT License.
|
// requirements defined in MIT License.
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Shared
|
||||||
import SQLite3
|
import SQLite3
|
||||||
|
|
||||||
public enum LMAssembly {
|
public enum vChewingLM {
|
||||||
enum FileErrors: Error {
|
enum FileErrors: Error {
|
||||||
case fileHandleError(String)
|
case fileHandleError(String)
|
||||||
}
|
}
|
||||||
|
@ -55,7 +56,7 @@ extension Array where Element == String {
|
||||||
sqlite3_prepare_v2(ptrDB, strStmt, -1, &ptrStmt, nil) == SQLITE_OK && sqlite3_step(ptrStmt) == SQLITE_DONE
|
sqlite3_prepare_v2(ptrDB, strStmt, -1, &ptrStmt, nil) == SQLITE_OK && sqlite3_step(ptrStmt) == SQLITE_DONE
|
||||||
}
|
}
|
||||||
guard thisResult else {
|
guard thisResult else {
|
||||||
vCLMLog("SQL Query Error. Statement: \(strStmt)")
|
vCLog("SQL Query Error. Statement: \(strStmt)")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,13 +83,3 @@ func performStatementSansResult(_ handler: (inout OpaquePointer?) -> Void) {
|
||||||
}
|
}
|
||||||
handler(&ptrStmt)
|
handler(&ptrStmt)
|
||||||
}
|
}
|
||||||
|
|
||||||
func vCLMLog(_ strPrint: StringLiteralType) {
|
|
||||||
guard let toLog = UserDefaults.standard.object(forKey: "_DebugMode") as? Bool else {
|
|
||||||
NSLog("vChewingDebug: %@", strPrint)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if toLog {
|
|
||||||
NSLog("vChewingDebug: %@", strPrint)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -57,8 +57,8 @@ final class InputTokenTests: XCTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
func testGeneratedResultsFromLMInstantiator() throws {
|
func testGeneratedResultsFromLMInstantiator() throws {
|
||||||
let instance = LMAssembly.LMInstantiator(isCHS: true)
|
let instance = vChewingLM.LMInstantiator(isCHS: true)
|
||||||
XCTAssertTrue(LMAssembly.LMInstantiator.connectToTestSQLDB())
|
XCTAssertTrue(vChewingLM.LMInstantiator.connectToTestSQLDB())
|
||||||
instance.setOptions { config in
|
instance.setOptions { config in
|
||||||
config.isCNSEnabled = false
|
config.isCNSEnabled = false
|
||||||
config.isSymbolEnabled = false
|
config.isSymbolEnabled = false
|
||||||
|
@ -70,6 +70,6 @@ final class InputTokenTests: XCTestCase {
|
||||||
)
|
)
|
||||||
let x = instance.unigramsFor(keyArray: ["ㄐㄧㄣ", "ㄊㄧㄢ", "ㄖˋ", "ㄑㄧˊ"]).description
|
let x = instance.unigramsFor(keyArray: ["ㄐㄧㄣ", "ㄊㄧㄢ", "ㄖˋ", "ㄑㄧˊ"]).description
|
||||||
print(x)
|
print(x)
|
||||||
LMAssembly.LMInstantiator.disconnectSQLDB()
|
vChewingLM.LMInstantiator.disconnectSQLDB()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ private let testDataPath: String = packageRootPath + "/Tests/TestCINData/"
|
||||||
final class LMCassetteTests: XCTestCase {
|
final class LMCassetteTests: XCTestCase {
|
||||||
func testCassetteLoadWubi86() throws {
|
func testCassetteLoadWubi86() throws {
|
||||||
let pathCINFile = testDataPath + "wubi.cin"
|
let pathCINFile = testDataPath + "wubi.cin"
|
||||||
var lmCassette = LMAssembly.LMCassette()
|
var lmCassette = vChewingLM.LMCassette()
|
||||||
NSLog("LMCassette: Start loading CIN.")
|
NSLog("LMCassette: Start loading CIN.")
|
||||||
lmCassette.open(pathCINFile)
|
lmCassette.open(pathCINFile)
|
||||||
NSLog("LMCassette: Finished loading CIN. Entries: \(lmCassette.count)")
|
NSLog("LMCassette: Finished loading CIN. Entries: \(lmCassette.count)")
|
||||||
|
@ -41,7 +41,7 @@ final class LMCassetteTests: XCTestCase {
|
||||||
|
|
||||||
func testCassetteLoadArray30() throws {
|
func testCassetteLoadArray30() throws {
|
||||||
let pathCINFile = testDataPath + "array30.cin2"
|
let pathCINFile = testDataPath + "array30.cin2"
|
||||||
var lmCassette = LMAssembly.LMCassette()
|
var lmCassette = vChewingLM.LMCassette()
|
||||||
NSLog("LMCassette: Start loading CIN.")
|
NSLog("LMCassette: Start loading CIN.")
|
||||||
lmCassette.open(pathCINFile)
|
lmCassette.open(pathCINFile)
|
||||||
NSLog("LMCassette: Finished loading CIN. Entries: \(lmCassette.count)")
|
NSLog("LMCassette: Finished loading CIN. Entries: \(lmCassette.count)")
|
||||||
|
|
|
@ -38,7 +38,7 @@ private let sampleData: String = #"""
|
||||||
|
|
||||||
final class LMCoreEXTests: XCTestCase {
|
final class LMCoreEXTests: XCTestCase {
|
||||||
func testLMCoreEXAsFactoryCoreDict() throws {
|
func testLMCoreEXAsFactoryCoreDict() throws {
|
||||||
var lmTest = LMAssembly.LMCoreEX(
|
var lmTest = vChewingLM.LMCoreEX(
|
||||||
reverse: false, consolidate: false, defaultScore: 0, forceDefaultScore: false
|
reverse: false, consolidate: false, defaultScore: 0, forceDefaultScore: false
|
||||||
)
|
)
|
||||||
lmTest.replaceData(textData: sampleData)
|
lmTest.replaceData(textData: sampleData)
|
||||||
|
|
|
@ -22,8 +22,8 @@ private let expectedReverseLookupResults: [String] = [
|
||||||
|
|
||||||
final class LMInstantiatorSQLTests: XCTestCase {
|
final class LMInstantiatorSQLTests: XCTestCase {
|
||||||
func testSQL() throws {
|
func testSQL() throws {
|
||||||
let instance = LMAssembly.LMInstantiator(isCHS: true)
|
let instance = vChewingLM.LMInstantiator(isCHS: true)
|
||||||
XCTAssertTrue(LMAssembly.LMInstantiator.connectToTestSQLDB())
|
XCTAssertTrue(vChewingLM.LMInstantiator.connectToTestSQLDB())
|
||||||
instance.setOptions { config in
|
instance.setOptions { config in
|
||||||
config.isCNSEnabled = false
|
config.isCNSEnabled = false
|
||||||
config.isSymbolEnabled = false
|
config.isSymbolEnabled = false
|
||||||
|
@ -41,24 +41,7 @@ final class LMInstantiatorSQLTests: XCTestCase {
|
||||||
XCTAssertEqual(instance.unigramsFor(keyArray: strRefutationKey).count, 10)
|
XCTAssertEqual(instance.unigramsFor(keyArray: strRefutationKey).count, 10)
|
||||||
XCTAssertEqual(instance.unigramsFor(keyArray: strBoobsKey).last?.description, "(☉☉,-13.0)")
|
XCTAssertEqual(instance.unigramsFor(keyArray: strBoobsKey).last?.description, "(☉☉,-13.0)")
|
||||||
// 再測試反查。
|
// 再測試反查。
|
||||||
XCTAssertEqual(LMAssembly.LMInstantiator.getFactoryReverseLookupData(with: "和"), expectedReverseLookupResults)
|
XCTAssertEqual(vChewingLM.LMInstantiator.getFactoryReverseLookupData(with: "和"), expectedReverseLookupResults)
|
||||||
LMAssembly.LMInstantiator.disconnectSQLDB()
|
vChewingLM.LMInstantiator.disconnectSQLDB()
|
||||||
}
|
|
||||||
|
|
||||||
func testCNSMask() throws {
|
|
||||||
let instance = LMAssembly.LMInstantiator(isCHS: false)
|
|
||||||
XCTAssertTrue(LMAssembly.LMInstantiator.connectToTestSQLDB())
|
|
||||||
instance.setOptions { config in
|
|
||||||
config.isCNSEnabled = false
|
|
||||||
config.isSymbolEnabled = false
|
|
||||||
config.filterNonCNSReadings = false
|
|
||||||
}
|
|
||||||
XCTAssertEqual(instance.unigramsFor(keyArray: ["ㄨㄟ"]).description, "[(危,-6.0)]")
|
|
||||||
XCTAssertEqual(instance.unigramsFor(keyArray: ["ㄨㄟˊ"]).description, "[(危,-6.0)]")
|
|
||||||
instance.setOptions { config in
|
|
||||||
config.filterNonCNSReadings = true
|
|
||||||
}
|
|
||||||
XCTAssertEqual(instance.unigramsFor(keyArray: ["ㄨㄟ"]).description, "[]")
|
|
||||||
XCTAssertEqual(instance.unigramsFor(keyArray: ["ㄨㄟˊ"]).description, "[(危,-6.0)]")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
//// (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, ["节", "洁", "杰"])
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -17,12 +17,12 @@ private let halfLife: Double = 5400
|
||||||
private let nullURL = URL(fileURLWithPath: "/dev/null")
|
private let nullURL = URL(fileURLWithPath: "/dev/null")
|
||||||
|
|
||||||
final class LMUserOverrideTests: XCTestCase {
|
final class LMUserOverrideTests: XCTestCase {
|
||||||
private func observe(who uom: LMAssembly.LMUserOverride, key: String, candidate: String, timestamp stamp: Double) {
|
private func observe(who uom: vChewingLM.LMUserOverride, key: String, candidate: String, timestamp stamp: Double) {
|
||||||
uom.doObservation(key: key, candidate: candidate, timestamp: stamp, forceHighScoreOverride: false, saveCallback: {})
|
uom.doObservation(key: key, candidate: candidate, timestamp: stamp, forceHighScoreOverride: false, saveCallback: {})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testUOM_1_BasicOps() throws {
|
func testUOM_1_BasicOps() throws {
|
||||||
let uom = LMAssembly.LMUserOverride(capacity: capacity, decayConstant: Double(halfLife), dataURL: nullURL)
|
let uom = vChewingLM.LMUserOverride(capacity: capacity, decayConstant: Double(halfLife), dataURL: nullURL)
|
||||||
let key = "((ㄕㄣˊ-ㄌㄧˇ-ㄌㄧㄥˊ-ㄏㄨㄚˊ,神里綾華),(ㄉㄜ˙,的),ㄍㄡˇ)"
|
let key = "((ㄕㄣˊ-ㄌㄧˇ-ㄌㄧㄥˊ-ㄏㄨㄚˊ,神里綾華),(ㄉㄜ˙,的),ㄍㄡˇ)"
|
||||||
let headReading = "ㄍㄡˇ"
|
let headReading = "ㄍㄡˇ"
|
||||||
let expectedSuggestion = "狗"
|
let expectedSuggestion = "狗"
|
||||||
|
@ -45,7 +45,7 @@ final class LMUserOverrideTests: XCTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
func testUOM_2_NewestAgainstRepeatedlyUsed() throws {
|
func testUOM_2_NewestAgainstRepeatedlyUsed() throws {
|
||||||
let uom = LMAssembly.LMUserOverride(capacity: capacity, decayConstant: Double(halfLife), dataURL: nullURL)
|
let uom = vChewingLM.LMUserOverride(capacity: capacity, decayConstant: Double(halfLife), dataURL: nullURL)
|
||||||
let key = "((ㄕㄣˊ-ㄌㄧˇ-ㄌㄧㄥˊ-ㄏㄨㄚˊ,神里綾華),(ㄉㄜ˙,的),ㄍㄡˇ)"
|
let key = "((ㄕㄣˊ-ㄌㄧˇ-ㄌㄧㄥˊ-ㄏㄨㄚˊ,神里綾華),(ㄉㄜ˙,的),ㄍㄡˇ)"
|
||||||
let headReading = "ㄍㄡˇ"
|
let headReading = "ㄍㄡˇ"
|
||||||
let valRepeatedlyUsed = "狗" // 更常用
|
let valRepeatedlyUsed = "狗" // 更常用
|
||||||
|
@ -74,7 +74,7 @@ final class LMUserOverrideTests: XCTestCase {
|
||||||
let b = (key: "((ㄆㄞˋ-ㄇㄥˊ,派蒙),(ㄉㄜ˙,的),ㄐㄧㄤˇ-ㄐㄧㄣ)", value: "伙食費", head: "ㄏㄨㄛˇ-ㄕˊ-ㄈㄟˋ")
|
let b = (key: "((ㄆㄞˋ-ㄇㄥˊ,派蒙),(ㄉㄜ˙,的),ㄐㄧㄤˇ-ㄐㄧㄣ)", value: "伙食費", head: "ㄏㄨㄛˇ-ㄕˊ-ㄈㄟˋ")
|
||||||
let c = (key: "((ㄍㄨㄛˊ-ㄅㄥ,國崩),(ㄉㄜ˙,的),ㄇㄠˋ-ㄗ˙)", value: "帽子", head: "ㄇㄠˋ-ㄗ˙")
|
let c = (key: "((ㄍㄨㄛˊ-ㄅㄥ,國崩),(ㄉㄜ˙,的),ㄇㄠˋ-ㄗ˙)", value: "帽子", head: "ㄇㄠˋ-ㄗ˙")
|
||||||
let d = (key: "((ㄌㄟˊ-ㄉㄧㄢˋ-ㄐㄧㄤ-ㄐㄩㄣ,雷電將軍),(ㄉㄜ˙,的),ㄐㄧㄠˇ-ㄔㄡˋ)", value: "腳臭", head: "ㄐㄧㄠˇ-ㄔㄡˋ")
|
let d = (key: "((ㄌㄟˊ-ㄉㄧㄢˋ-ㄐㄧㄤ-ㄐㄩㄣ,雷電將軍),(ㄉㄜ˙,的),ㄐㄧㄠˇ-ㄔㄡˋ)", value: "腳臭", head: "ㄐㄧㄠˇ-ㄔㄡˋ")
|
||||||
let uom = LMAssembly.LMUserOverride(capacity: 2, decayConstant: Double(halfLife), dataURL: nullURL)
|
let uom = vChewingLM.LMUserOverride(capacity: 2, decayConstant: Double(halfLife), dataURL: nullURL)
|
||||||
observe(who: uom, key: a.key, candidate: a.value, timestamp: nowTimeStamp)
|
observe(who: uom, key: a.key, candidate: a.value, timestamp: nowTimeStamp)
|
||||||
observe(who: uom, key: b.key, candidate: b.value, timestamp: nowTimeStamp + halfLife * 1)
|
observe(who: uom, key: b.key, candidate: b.value, timestamp: nowTimeStamp + halfLife * 1)
|
||||||
observe(who: uom, key: c.key, candidate: c.value, timestamp: nowTimeStamp + halfLife * 2)
|
observe(who: uom, key: c.key, candidate: c.value, timestamp: nowTimeStamp + halfLife * 2)
|
||||||
|
|
|
@ -13,7 +13,7 @@ import XCTest
|
||||||
|
|
||||||
final class LMInstantiatorNumericPadTests: XCTestCase {
|
final class LMInstantiatorNumericPadTests: XCTestCase {
|
||||||
func testSQL() throws {
|
func testSQL() throws {
|
||||||
let instance = LMAssembly.LMInstantiator(isCHS: true)
|
let instance = vChewingLM.LMInstantiator(isCHS: true)
|
||||||
instance.setOptions { config in
|
instance.setOptions { config in
|
||||||
config.numPadFWHWStatus = nil
|
config.numPadFWHWStatus = nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,12 +17,10 @@ let package = Package(
|
||||||
.package(path: "../HangarRash_SwiftyCapsLockToggler"),
|
.package(path: "../HangarRash_SwiftyCapsLockToggler"),
|
||||||
.package(path: "../Jad_BookmarkManager"),
|
.package(path: "../Jad_BookmarkManager"),
|
||||||
.package(path: "../Qwertyyb_ShiftKeyUpChecker"),
|
.package(path: "../Qwertyyb_ShiftKeyUpChecker"),
|
||||||
.package(path: "../vChewing_BrailleSputnik"),
|
|
||||||
.package(path: "../vChewing_CandidateWindow"),
|
.package(path: "../vChewing_CandidateWindow"),
|
||||||
.package(path: "../vChewing_OSFrameworkImpl"),
|
.package(path: "../vChewing_CocoaExtension"),
|
||||||
.package(path: "../vChewing_Hotenka"),
|
.package(path: "../vChewing_Hotenka"),
|
||||||
.package(path: "../vChewing_IMKUtils"),
|
.package(path: "../vChewing_IMKUtils"),
|
||||||
.package(path: "../vChewing_KimoDataReader"),
|
|
||||||
.package(path: "../vChewing_LangModelAssembly"),
|
.package(path: "../vChewing_LangModelAssembly"),
|
||||||
.package(path: "../vChewing_Megrez"),
|
.package(path: "../vChewing_Megrez"),
|
||||||
.package(path: "../vChewing_NotifierUI"),
|
.package(path: "../vChewing_NotifierUI"),
|
||||||
|
@ -39,14 +37,12 @@ let package = Package(
|
||||||
.target(
|
.target(
|
||||||
name: "MainAssembly",
|
name: "MainAssembly",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.product(name: "BrailleSputnik", package: "vChewing_BrailleSputnik"),
|
|
||||||
.product(name: "BookmarkManager", package: "Jad_BookmarkManager"),
|
.product(name: "BookmarkManager", package: "Jad_BookmarkManager"),
|
||||||
.product(name: "CandidateWindow", package: "vChewing_CandidateWindow"),
|
.product(name: "CandidateWindow", package: "vChewing_CandidateWindow"),
|
||||||
.product(name: "OSFrameworkImpl", package: "vChewing_OSFrameworkImpl"),
|
.product(name: "CocoaExtension", package: "vChewing_CocoaExtension"),
|
||||||
.product(name: "FolderMonitor", package: "DanielGalasko_FolderMonitor"),
|
.product(name: "FolderMonitor", package: "DanielGalasko_FolderMonitor"),
|
||||||
.product(name: "Hotenka", package: "vChewing_Hotenka"),
|
.product(name: "Hotenka", package: "vChewing_Hotenka"),
|
||||||
.product(name: "IMKUtils", package: "vChewing_IMKUtils"),
|
.product(name: "IMKUtils", package: "vChewing_IMKUtils"),
|
||||||
.product(name: "KimoDataReader", package: "vChewing_KimoDataReader"),
|
|
||||||
.product(name: "LangModelAssembly", package: "vChewing_LangModelAssembly"),
|
.product(name: "LangModelAssembly", package: "vChewing_LangModelAssembly"),
|
||||||
.product(name: "Megrez", package: "vChewing_Megrez"),
|
.product(name: "Megrez", package: "vChewing_Megrez"),
|
||||||
.product(name: "NotifierUI", package: "vChewing_NotifierUI"),
|
.product(name: "NotifierUI", package: "vChewing_NotifierUI"),
|
||||||
|
|
|
@ -7,41 +7,23 @@
|
||||||
// requirements defined in MIT License.
|
// requirements defined in MIT License.
|
||||||
|
|
||||||
import AppKit
|
import AppKit
|
||||||
import Shared
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
@available(macOS 12, *)
|
||||||
public class CtlAboutUI: NSWindowController, NSWindowDelegate {
|
public class CtlAboutUI: NSWindowController, NSWindowDelegate {
|
||||||
public static var shared: CtlAboutUI?
|
public static var shared: CtlAboutUI?
|
||||||
private var viewController: NSViewController?
|
|
||||||
var useLegacyView: Bool = false
|
|
||||||
|
|
||||||
public init(forceLegacy: Bool = false) {
|
public static func show() {
|
||||||
useLegacyView = forceLegacy
|
if shared == nil {
|
||||||
let newWindow = NSWindow(
|
let newWindow = NSWindow(
|
||||||
contentRect: CGRect(x: 401, y: 295, width: 577, height: 568),
|
contentRect: CGRect(x: 401, y: 295, width: 577, height: 568),
|
||||||
styleMask: [.titled, .closable, .miniaturizable],
|
styleMask: [.titled, .closable, .miniaturizable],
|
||||||
backing: .buffered, defer: true
|
backing: .buffered, defer: true
|
||||||
)
|
)
|
||||||
super.init(window: newWindow)
|
let newInstance = CtlAboutUI(window: newWindow)
|
||||||
guard #available(macOS 12, *), !useLegacyView else {
|
|
||||||
viewController = VwrAboutCocoa()
|
|
||||||
viewController?.loadView()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
super.init(coder: coder)
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func show() {
|
|
||||||
let forceLegacy = NSEvent.modifierFlags == .option
|
|
||||||
if shared == nil {
|
|
||||||
let newInstance = CtlAboutUI(forceLegacy: forceLegacy)
|
|
||||||
shared = newInstance
|
shared = newInstance
|
||||||
}
|
}
|
||||||
guard let shared = shared, let sharedWindow = shared.window else { return }
|
guard let shared = shared, let sharedWindow = shared.window else { return }
|
||||||
shared.useLegacyView = forceLegacy
|
|
||||||
sharedWindow.delegate = shared
|
sharedWindow.delegate = shared
|
||||||
if !sharedWindow.isVisible {
|
if !sharedWindow.isVisible {
|
||||||
shared.windowDidLoad()
|
shared.windowDidLoad()
|
||||||
|
@ -55,28 +37,6 @@ public class CtlAboutUI: NSWindowController, NSWindowDelegate {
|
||||||
|
|
||||||
override public func windowDidLoad() {
|
override public func windowDidLoad() {
|
||||||
super.windowDidLoad()
|
super.windowDidLoad()
|
||||||
guard let window = window else { return }
|
|
||||||
if #available(macOS 12, *), !useLegacyView {
|
|
||||||
windowDidLoadSwiftUI()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let theViewController = viewController ?? VwrAboutCocoa()
|
|
||||||
viewController = theViewController
|
|
||||||
window.contentViewController = viewController
|
|
||||||
let size = theViewController.view.fittingSize
|
|
||||||
window.setPosition(vertical: .top, horizontal: .left, padding: 20)
|
|
||||||
window.setFrame(.init(origin: window.frame.origin, size: size), display: true)
|
|
||||||
window.standardWindowButton(.closeButton)?.isHidden = true
|
|
||||||
window.standardWindowButton(.miniaturizeButton)?.isHidden = true
|
|
||||||
window.standardWindowButton(.zoomButton)?.isHidden = true
|
|
||||||
if #available(macOS 10.10, *) {
|
|
||||||
window.titlebarAppearsTransparent = true
|
|
||||||
}
|
|
||||||
window.title = "i18n:aboutWindow.ABOUT_APP_TITLE_FULL".localized + " (v\(IMEApp.appMainVersionLabel.joined(separator: " Build ")))"
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(macOS 12, *)
|
|
||||||
private func windowDidLoadSwiftUI() {
|
|
||||||
window?.setPosition(vertical: .top, horizontal: .left, padding: 20)
|
window?.setPosition(vertical: .top, horizontal: .left, padding: 20)
|
||||||
window?.standardWindowButton(.closeButton)?.isHidden = true
|
window?.standardWindowButton(.closeButton)?.isHidden = true
|
||||||
window?.standardWindowButton(.miniaturizeButton)?.isHidden = true
|
window?.standardWindowButton(.miniaturizeButton)?.isHidden = true
|
||||||
|
|
|
@ -1,215 +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 AppKit
|
|
||||||
import Foundation
|
|
||||||
import Shared
|
|
||||||
|
|
||||||
public extension VwrAboutCocoa {
|
|
||||||
static let copyrightLabel = Bundle.main.localizedInfoDictionary?["NSHumanReadableCopyright"] as? String ?? "BAD_COPYRIGHT_LABEL"
|
|
||||||
static let eulaContent = Bundle.main.localizedInfoDictionary?["CFEULAContent"] as? String ?? "BAD_EULA_CONTENT"
|
|
||||||
static let eulaContentUpstream = Bundle.main.infoDictionary?["CFUpstreamEULAContent"] as? String ?? "BAD_EULA_UPSTREAM"
|
|
||||||
}
|
|
||||||
|
|
||||||
public class VwrAboutCocoa: NSViewController {
|
|
||||||
let windowWidth: CGFloat = 533
|
|
||||||
let contentWidth: CGFloat = 510
|
|
||||||
let imgWidth: CGFloat = 63
|
|
||||||
|
|
||||||
override public func loadView() {
|
|
||||||
view = body ?? .init()
|
|
||||||
(view as? NSStackView)?.alignment = .centerX
|
|
||||||
view.makeSimpleConstraint(.width, relation: .equal, value: windowWidth)
|
|
||||||
}
|
|
||||||
|
|
||||||
var appNameAndVersionString: NSAttributedString {
|
|
||||||
let strResult = NSMutableAttributedString(string: "i18n:aboutWindow.APP_NAME".localized)
|
|
||||||
strResult.addAttribute(
|
|
||||||
.font,
|
|
||||||
value: {
|
|
||||||
if #available(macOS 10.11, *) {
|
|
||||||
return NSFont.systemFont(ofSize: 12, weight: .bold)
|
|
||||||
}
|
|
||||||
return NSFont.boldSystemFont(ofSize: 12)
|
|
||||||
}(),
|
|
||||||
range: .init(location: 0, length: strResult.length)
|
|
||||||
)
|
|
||||||
let strVersion = NSMutableAttributedString(string: " \(versionString)")
|
|
||||||
strVersion.addAttribute(
|
|
||||||
.font,
|
|
||||||
value: NSFont.systemFont(ofSize: 11),
|
|
||||||
range: .init(location: 0, length: strVersion.length)
|
|
||||||
)
|
|
||||||
strResult.append(strVersion)
|
|
||||||
strResult.addAttribute(
|
|
||||||
.kern,
|
|
||||||
value: 0,
|
|
||||||
range: .init(location: 0, length: strResult.length)
|
|
||||||
)
|
|
||||||
return strResult
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: NSView? {
|
|
||||||
NSStackView.buildSection(width: contentWidth - 18) {
|
|
||||||
NSStackView.build(.horizontal) {
|
|
||||||
bannerImage
|
|
||||||
NSStackView.build(.vertical) {
|
|
||||||
appNameAndVersionString.makeNSLabel(fixWidth: contentWidth - imgWidth - 10)
|
|
||||||
makeFormattedLabel(
|
|
||||||
verbatim: "i18n:aboutWindow.APP_DERIVED_FROM".localized
|
|
||||||
+ "\n"
|
|
||||||
+ Self.copyrightLabel,
|
|
||||||
size: 11,
|
|
||||||
isBold: false, fixWidth: contentWidth - imgWidth - 10
|
|
||||||
)
|
|
||||||
makeFormattedLabel(
|
|
||||||
verbatim: "i18n:aboutWindow.DEV_CREW".localized,
|
|
||||||
size: 11,
|
|
||||||
isBold: false, fixWidth: contentWidth - imgWidth - 10
|
|
||||||
)
|
|
||||||
makeFormattedLabel(
|
|
||||||
verbatim: "i18n:aboutWindow.LICENSE_TITLE".localized,
|
|
||||||
size: 11,
|
|
||||||
isBold: false, fixWidth: contentWidth - imgWidth - 10
|
|
||||||
)
|
|
||||||
eulaBox
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NSStackView.build(.horizontal) {
|
|
||||||
NSStackView.build(.vertical) {
|
|
||||||
"i18n:aboutWindow.DISCLAIMER_TEXT".makeNSLabel(
|
|
||||||
descriptive: true, fixWidth: contentWidth - 120
|
|
||||||
)
|
|
||||||
NSView()
|
|
||||||
}
|
|
||||||
var verticalButtonStackSpacing: CGFloat? = 4
|
|
||||||
if #unavailable(macOS 10.10) {
|
|
||||||
verticalButtonStackSpacing = nil
|
|
||||||
}
|
|
||||||
NSStackView.build(.vertical, spacing: verticalButtonStackSpacing, width: 114) {
|
|
||||||
addKeyEquivalent(
|
|
||||||
NSButton(
|
|
||||||
"i18n:aboutWindow.OK_BUTTON",
|
|
||||||
target: self, action: #selector(btnOKAction(_:))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
NSButton(
|
|
||||||
"i18n:aboutWindow.WEBSITE_BUTTON",
|
|
||||||
target: self, action: #selector(btnWebSiteAction(_:))
|
|
||||||
)
|
|
||||||
NSButton(
|
|
||||||
"i18n:aboutWindow.BUGREPORT_BUTTON",
|
|
||||||
target: self, action: #selector(btnBugReportAction(_:))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}?.withInsets(
|
|
||||||
{
|
|
||||||
if #available(macOS 10.10, *) {
|
|
||||||
return .new(all: 20, top: 0, bottom: 24)
|
|
||||||
} else {
|
|
||||||
return .new(all: 20, top: 10, bottom: 24)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var versionString: String {
|
|
||||||
"v\(IMEApp.appMainVersionLabel.joined(separator: " Build ")) - \(IMEApp.appSignedDateLabel)"
|
|
||||||
}
|
|
||||||
|
|
||||||
var bannerImage: NSImageView {
|
|
||||||
let maybeImg = NSImage(named: "AboutBanner")
|
|
||||||
let imgIsNull = maybeImg == nil
|
|
||||||
let img = maybeImg ?? .init(size: .init(width: 63, height: 310))
|
|
||||||
let result = NSImageView()
|
|
||||||
result.image = img
|
|
||||||
result.makeSimpleConstraint(.width, relation: .equal, value: 63)
|
|
||||||
result.makeSimpleConstraint(.height, relation: .equal, value: 310)
|
|
||||||
if imgIsNull {
|
|
||||||
result.wantsLayer = true
|
|
||||||
result.layer?.backgroundColor = NSColor.black.cgColor
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeFormattedLabel(
|
|
||||||
verbatim: String,
|
|
||||||
size: CGFloat = 12,
|
|
||||||
isBold: Bool = false,
|
|
||||||
fixWidth: CGFloat? = nil
|
|
||||||
) -> NSTextField {
|
|
||||||
let attrStr = NSMutableAttributedString(string: verbatim)
|
|
||||||
attrStr.addAttribute(
|
|
||||||
.kern,
|
|
||||||
value: 0,
|
|
||||||
range: .init(location: 0, length: attrStr.length)
|
|
||||||
)
|
|
||||||
attrStr.addAttribute(
|
|
||||||
.font,
|
|
||||||
value: {
|
|
||||||
guard isBold else { return NSFont.systemFont(ofSize: size) }
|
|
||||||
if #available(macOS 10.11, *) {
|
|
||||||
return NSFont.systemFont(ofSize: size, weight: .bold)
|
|
||||||
}
|
|
||||||
return NSFont.boldSystemFont(ofSize: size)
|
|
||||||
}(),
|
|
||||||
range: .init(location: 0, length: attrStr.length)
|
|
||||||
)
|
|
||||||
return attrStr.makeNSLabel(fixWidth: fixWidth)
|
|
||||||
}
|
|
||||||
|
|
||||||
var eulaBox: NSScrollView {
|
|
||||||
let textView = NSTextView()
|
|
||||||
let clipView = NSClipView()
|
|
||||||
let scrollView = NSScrollView()
|
|
||||||
textView.autoresizingMask = [.width, .height]
|
|
||||||
textView.isEditable = false
|
|
||||||
textView.isRichText = false
|
|
||||||
textView.isSelectable = true
|
|
||||||
textView.isVerticallyResizable = true
|
|
||||||
textView.smartInsertDeleteEnabled = true
|
|
||||||
textView.font = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize)
|
|
||||||
textView.string = Self.eulaContent + "\n" + Self.eulaContentUpstream
|
|
||||||
clipView.documentView = textView
|
|
||||||
clipView.autoresizingMask = [.width, .height]
|
|
||||||
clipView.drawsBackground = false
|
|
||||||
scrollView.contentView = clipView
|
|
||||||
scrollView.makeSimpleConstraint(.width, relation: .equal, value: 430)
|
|
||||||
scrollView.hasVerticalScroller = true
|
|
||||||
scrollView.hasHorizontalScroller = false
|
|
||||||
scrollView.scrollerStyle = .legacy
|
|
||||||
return scrollView
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult func addKeyEquivalent(_ button: NSButton) -> NSButton {
|
|
||||||
button.keyEquivalent = String(NSEvent.SpecialKey.carriageReturn.unicodeScalar)
|
|
||||||
return button
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func btnOKAction(_: NSControl) {
|
|
||||||
CtlAboutUI.shared?.window?.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func btnWebSiteAction(_: NSControl) {
|
|
||||||
if let url = URL(string: "https://vchewing.github.io/") {
|
|
||||||
NSWorkspace.shared.open(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func btnBugReportAction(_: NSControl) {
|
|
||||||
if let url = URL(string: "https://vchewing.github.io/BUGREPORT.html") {
|
|
||||||
NSWorkspace.shared.open(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(macOS 14.0, *)
|
|
||||||
#Preview(traits: .fixedLayout(width: 533, height: 550)) {
|
|
||||||
VwrAboutCocoa()
|
|
||||||
}
|
|
|
@ -7,18 +7,14 @@
|
||||||
// requirements defined in MIT License.
|
// requirements defined in MIT License.
|
||||||
|
|
||||||
import AppKit
|
import AppKit
|
||||||
import Shared
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
public struct VwrAboutUI {
|
|
||||||
public static var copyrightLabel: String { VwrAboutCocoa.copyrightLabel }
|
|
||||||
public static var eulaContent: String { VwrAboutCocoa.eulaContent }
|
|
||||||
public static var eulaContentUpstream: String { VwrAboutCocoa.eulaContentUpstream }
|
|
||||||
let foobar = "FOO_BAR"
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(macOS 12, *)
|
@available(macOS 12, *)
|
||||||
extension VwrAboutUI: View {
|
public struct VwrAboutUI: View {
|
||||||
|
public static let copyrightLabel = Bundle.main.localizedInfoDictionary?["NSHumanReadableCopyright"] as? String ?? "BAD_COPYRIGHT_LABEL"
|
||||||
|
public static let eulaContent = Bundle.main.localizedInfoDictionary?["CFEULAContent"] as? String ?? "BAD_EULA_CONTENT"
|
||||||
|
public static let eulaContentUpstream = Bundle.main.infoDictionary?["CFUpstreamEULAContent"] as? String ?? "BAD_EULA_UPSTREAM"
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
GroupBox {
|
GroupBox {
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
@ -33,7 +29,6 @@ extension VwrAboutUI: View {
|
||||||
Text("v\(IMEApp.appMainVersionLabel.joined(separator: " Build ")) - \(IMEApp.appSignedDateLabel)").lineLimit(1)
|
Text("v\(IMEApp.appMainVersionLabel.joined(separator: " Build ")) - \(IMEApp.appSignedDateLabel)").lineLimit(1)
|
||||||
}.fixedSize()
|
}.fixedSize()
|
||||||
Text("i18n:aboutWindow.APP_DERIVED_FROM").font(.custom("Tahoma", size: 11))
|
Text("i18n:aboutWindow.APP_DERIVED_FROM").font(.custom("Tahoma", size: 11))
|
||||||
Text(Self.copyrightLabel).font(.custom("Tahoma", size: 11))
|
|
||||||
Text("i18n:aboutWindow.DEV_CREW").font(.custom("Tahoma", size: 11)).padding([.vertical], 2)
|
Text("i18n:aboutWindow.DEV_CREW").font(.custom("Tahoma", size: 11)).padding([.vertical], 2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,7 @@ extension AppDelegate {
|
||||||
// 先執行 initUserLangModels() 可以在目標辭典檔案不存在的情況下先行生成空白範本檔案。
|
// 先執行 initUserLangModels() 可以在目標辭典檔案不存在的情況下先行生成空白範本檔案。
|
||||||
if PrefMgr.shared.shouldAutoReloadUserDataFiles || forced { LMMgr.initUserLangModels() }
|
if PrefMgr.shared.shouldAutoReloadUserDataFiles || forced { LMMgr.initUserLangModels() }
|
||||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
|
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
|
||||||
|
if #available(macOS 10.15, *) { FileObserveProject.shared.touch() }
|
||||||
if PrefMgr.shared.phraseEditorAutoReloadExternalModifications {
|
if PrefMgr.shared.phraseEditorAutoReloadExternalModifications {
|
||||||
Broadcaster.shared.eventForReloadingPhraseEditor = .init()
|
Broadcaster.shared.eventForReloadingPhraseEditor = .init()
|
||||||
}
|
}
|
||||||
|
@ -69,8 +70,6 @@ public extension AppDelegate {
|
||||||
|
|
||||||
SpeechSputnik.shared.refreshStatus() // 根據現狀條件決定是否初期化語音引擎。
|
SpeechSputnik.shared.refreshStatus() // 根據現狀條件決定是否初期化語音引擎。
|
||||||
|
|
||||||
CandidateTextService.enableFinalSanityCheck()
|
|
||||||
|
|
||||||
// 一旦發現與使用者半衰模組的觀察行為有關的崩潰標記被開啟:
|
// 一旦發現與使用者半衰模組的觀察行為有關的崩潰標記被開啟:
|
||||||
// 如果有開啟 Debug 模式的話,就將既有的半衰記憶資料檔案更名+打上當時的時間戳。
|
// 如果有開啟 Debug 模式的話,就將既有的半衰記憶資料檔案更名+打上當時的時間戳。
|
||||||
// 如果沒有開啟 Debug 模式的話,則將半衰記憶資料直接清空。
|
// 如果沒有開啟 Debug 模式的話,則將半衰記憶資料直接清空。
|
||||||
|
@ -135,9 +134,12 @@ public extension AppDelegate {
|
||||||
NSApp.popup()
|
NSApp.popup()
|
||||||
guard result == NSApplication.ModalResponse.alertFirstButtonReturn else { return }
|
guard result == NSApplication.ModalResponse.alertFirstButtonReturn else { return }
|
||||||
let url = URL(fileURLWithPath: LMMgr.dataFolderPath(isDefaultFolder: true))
|
let url = URL(fileURLWithPath: LMMgr.dataFolderPath(isDefaultFolder: true))
|
||||||
FileOpenMethod.finder.open(url: url)
|
guard let finderURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: "com.apple.finder") else { return }
|
||||||
|
let configuration = NSWorkspace.OpenConfiguration()
|
||||||
|
configuration.promptsUserIfNeeded = true
|
||||||
|
NSWorkspace.shared.open([url], withApplicationAt: finderURL, configuration: configuration)
|
||||||
Uninstaller.uninstall(
|
Uninstaller.uninstall(
|
||||||
selfKill: true, defaultDataFolderPath: LMMgr.dataFolderPath(isDefaultFolder: true)
|
isSudo: false, selfKill: true, defaultDataFolderPath: LMMgr.dataFolderPath(isDefaultFolder: true)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,7 +149,7 @@ public extension AppDelegate {
|
||||||
guard let currentMemorySizeInBytes = NSApplication.memoryFootprint else { return 0 }
|
guard let currentMemorySizeInBytes = NSApplication.memoryFootprint else { return 0 }
|
||||||
let currentMemorySize: Double = (Double(currentMemorySizeInBytes) / 1024 / 1024).rounded(toPlaces: 1)
|
let currentMemorySize: Double = (Double(currentMemorySizeInBytes) / 1024 / 1024).rounded(toPlaces: 1)
|
||||||
switch currentMemorySize {
|
switch currentMemorySize {
|
||||||
case 1024...:
|
case 384...:
|
||||||
vCLog("WARNING: EXCESSIVE MEMORY FOOTPRINT (\(currentMemorySize)MB).")
|
vCLog("WARNING: EXCESSIVE MEMORY FOOTPRINT (\(currentMemorySize)MB).")
|
||||||
let msgPackage = UNMutableNotificationContent()
|
let msgPackage = UNMutableNotificationContent()
|
||||||
msgPackage.title = NSLocalizedString("vChewing", comment: "")
|
msgPackage.title = NSLocalizedString("vChewing", comment: "")
|
||||||
|
@ -169,10 +171,4 @@ public extension AppDelegate {
|
||||||
}
|
}
|
||||||
return currentMemorySize
|
return currentMemorySize
|
||||||
}
|
}
|
||||||
|
|
||||||
// New About Window
|
|
||||||
@IBAction func about(_: Any) {
|
|
||||||
CtlAboutUI.show()
|
|
||||||
NSApp.popup()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,170 +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 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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -7,7 +7,6 @@
|
||||||
// requirements defined in MIT License.
|
// requirements defined in MIT License.
|
||||||
|
|
||||||
import Hotenka
|
import Hotenka
|
||||||
import Shared
|
|
||||||
|
|
||||||
public enum ChineseConverter {
|
public enum ChineseConverter {
|
||||||
public static let shared = HotenkaChineseConverter(
|
public static let shared = HotenkaChineseConverter(
|
||||||
|
|
|
@ -1,62 +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 AppKit
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
public class CtlClientListMgr: NSWindowController {
|
|
||||||
let viewController = VwrClientListMgr()
|
|
||||||
|
|
||||||
public static var shared: CtlClientListMgr?
|
|
||||||
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.loadView()
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
super.init(coder: coder)
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func show() {
|
|
||||||
if shared == nil {
|
|
||||||
shared = CtlClientListMgr()
|
|
||||||
}
|
|
||||||
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 = "Client Manager".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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import AppKit
|
import AppKit
|
||||||
import Carbon
|
import Carbon
|
||||||
|
import Shared
|
||||||
|
|
||||||
// MARK: - Top-level Enums relating to Input Mode and Language Supports.
|
// MARK: - Top-level Enums relating to Input Mode and Language Supports.
|
||||||
|
|
||||||
|
@ -51,7 +52,7 @@ public enum IMEApp {
|
||||||
// MARK: - 輸入法的當前的簡繁體中文模式
|
// MARK: - 輸入法的當前的簡繁體中文模式
|
||||||
|
|
||||||
public static var currentInputMode: Shared.InputMode {
|
public static var currentInputMode: Shared.InputMode {
|
||||||
.init(rawValue: PrefMgr().mostRecentInputMode) ?? .imeModeNULL
|
.init(rawValue: PrefMgr.shared.mostRecentInputMode) ?? .imeModeNULL
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 當前鍵盤是否是 JIS 佈局
|
/// 當前鍵盤是否是 JIS 佈局
|
||||||
|
@ -61,10 +62,9 @@ public enum IMEApp {
|
||||||
|
|
||||||
/// Fart or Beep?
|
/// Fart or Beep?
|
||||||
public static func buzz() {
|
public static func buzz() {
|
||||||
let prefs = PrefMgr()
|
if PrefMgr.shared.isDebugModeEnabled {
|
||||||
if prefs.isDebugModeEnabled {
|
NSSound.buzz(fart: !PrefMgr.shared.shouldNotFartInLieuOfBeep)
|
||||||
NSSound.buzz(fart: !prefs.shouldNotFartInLieuOfBeep)
|
} else if !PrefMgr.shared.shouldNotFartInLieuOfBeep {
|
||||||
} else if !prefs.shouldNotFartInLieuOfBeep {
|
|
||||||
NSSound.buzz(fart: true)
|
NSSound.buzz(fart: true)
|
||||||
} else {
|
} else {
|
||||||
NSSound.beep()
|
NSSound.beep()
|
|
@ -200,8 +200,7 @@ public extension IMEState {
|
||||||
case .ofCandidates where cursor != marker: return data.attributedStringMarking(for: session)
|
case .ofCandidates where cursor != marker: return data.attributedStringMarking(for: session)
|
||||||
case .ofCandidates where cursor == marker: break
|
case .ofCandidates where cursor == marker: break
|
||||||
case .ofAssociates: return data.attributedStringPlaceholder(for: session)
|
case .ofAssociates: return data.attributedStringPlaceholder(for: session)
|
||||||
case .ofSymbolTable where displayedText.isEmpty || node.containsCandidateServices:
|
case .ofSymbolTable where displayedText.isEmpty: return data.attributedStringPlaceholder(for: session)
|
||||||
return data.attributedStringPlaceholder(for: session)
|
|
||||||
case .ofSymbolTable where !displayedText.isEmpty: break
|
case .ofSymbolTable where !displayedText.isEmpty: break
|
||||||
default: break
|
default: break
|
||||||
}
|
}
|
||||||
|
|
|
@ -212,7 +212,7 @@ public extension IMEStateData {
|
||||||
subNeta = Tekkon.cnvPhonaToHanyuPinyin(targetJoined: subNeta)
|
subNeta = Tekkon.cnvPhonaToHanyuPinyin(targetJoined: subNeta)
|
||||||
subNeta = Tekkon.cnvHanyuPinyinToTextbookStyle(targetJoined: subNeta)
|
subNeta = Tekkon.cnvHanyuPinyinToTextbookStyle(targetJoined: subNeta)
|
||||||
} else {
|
} else {
|
||||||
subNeta = Tekkon.cnvPhonaToTextbookStyle(target: subNeta)
|
subNeta = Tekkon.cnvPhonaToTextbookReading(target: subNeta)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
arrOutput.append(subNeta)
|
arrOutput.append(subNeta)
|
||||||
|
|
|
@ -1,125 +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 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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,77 +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
|
|
||||||
import SwiftExtension
|
|
||||||
|
|
||||||
// MARK: - Typing Method
|
|
||||||
|
|
||||||
public extension InputHandler {
|
|
||||||
enum TypingMethod: Int, CaseIterable {
|
|
||||||
case vChewingFactory // 自動指派: 0
|
|
||||||
case codePoint // 自動指派: 1
|
|
||||||
case haninKeyboardSymbol // 自動指派: 2
|
|
||||||
|
|
||||||
mutating func revolveNext() {
|
|
||||||
var theInt = rawValue
|
|
||||||
theInt.revolveAsIndex(with: Self.allCases)
|
|
||||||
guard let nextMethod = TypingMethod(rawValue: theInt) else { return }
|
|
||||||
self = nextMethod
|
|
||||||
}
|
|
||||||
|
|
||||||
func getTooltip(vertical: Bool = false) -> String {
|
|
||||||
switch self {
|
|
||||||
case .vChewingFactory: return ""
|
|
||||||
case .codePoint:
|
|
||||||
let commonTerm = NSMutableString()
|
|
||||||
commonTerm.insert("Code Point Input.".localized, at: 0)
|
|
||||||
if !vertical, let initials = IMEApp.currentInputMode.nonUTFEncodingInitials {
|
|
||||||
commonTerm.insert("[\(initials)] ", at: 0)
|
|
||||||
}
|
|
||||||
return commonTerm.description
|
|
||||||
case .haninKeyboardSymbol:
|
|
||||||
return "\("Hanin Keyboard Symbol Input.".localized)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Handle Rotation Toggles
|
|
||||||
|
|
||||||
public extension InputHandler {
|
|
||||||
@discardableResult func revolveTypingMethod(to specifiedMethod: TypingMethod? = nil) -> Bool {
|
|
||||||
guard let delegate = delegate else { return false }
|
|
||||||
var newMethod = currentTypingMethod
|
|
||||||
if let specified = specifiedMethod {
|
|
||||||
newMethod = specified
|
|
||||||
} else {
|
|
||||||
newMethod.revolveNext()
|
|
||||||
}
|
|
||||||
/// 接下來這行必須這樣 defer 處理,
|
|
||||||
/// 因為再接下來的 switch newMethod 的過程會影響到 currentTypingMethod 參數。
|
|
||||||
defer {
|
|
||||||
currentTypingMethod = newMethod
|
|
||||||
}
|
|
||||||
switch newMethod {
|
|
||||||
case .vChewingFactory:
|
|
||||||
delegate.switchState(IMEState.ofAbortion())
|
|
||||||
return true
|
|
||||||
case .codePoint:
|
|
||||||
strCodePointBuffer.removeAll()
|
|
||||||
case .haninKeyboardSymbol: break
|
|
||||||
}
|
|
||||||
var updatedState = generateStateOfInputting(sansReading: true)
|
|
||||||
delegate.switchState(IMEState.ofCommitting(textToCommit: updatedState.displayedText))
|
|
||||||
updatedState = generateStateOfInputting(guarded: true)
|
|
||||||
updatedState.tooltipDuration = 0
|
|
||||||
updatedState.tooltip = newMethod.getTooltip(vertical: delegate.isVerticalTyping)
|
|
||||||
delegate.switchState(updatedState)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -19,8 +19,10 @@ import Tekkon
|
||||||
// MARK: - InputHandler 自身協定 (Protocol).
|
// MARK: - InputHandler 自身協定 (Protocol).
|
||||||
|
|
||||||
public protocol InputHandlerProtocol {
|
public protocol InputHandlerProtocol {
|
||||||
var currentLM: LMAssembly.LMInstantiator { get set }
|
var currentLM: vChewingLM.LMInstantiator { get set }
|
||||||
|
var currentUOM: vChewingLM.LMUserOverride { get set }
|
||||||
var delegate: InputHandlerDelegate? { get set }
|
var delegate: InputHandlerDelegate? { get set }
|
||||||
|
var composer: Tekkon.Composer { get set }
|
||||||
var keySeparator: String { get }
|
var keySeparator: String { get }
|
||||||
static var keySeparator: String { get }
|
static var keySeparator: String { get }
|
||||||
var isCompositorEmpty: Bool { get }
|
var isCompositorEmpty: Bool { get }
|
||||||
|
@ -29,7 +31,7 @@ public protocol InputHandlerProtocol {
|
||||||
func clearComposerAndCalligrapher()
|
func clearComposerAndCalligrapher()
|
||||||
func ensureKeyboardParser()
|
func ensureKeyboardParser()
|
||||||
func triageInput(event input: InputSignalProtocol) -> Bool
|
func triageInput(event input: InputSignalProtocol) -> Bool
|
||||||
func generateStateOfCandidates(dodge: Bool) -> IMEStateProtocol
|
func generateStateOfCandidates() -> IMEStateProtocol
|
||||||
func generateStateOfInputting(sansReading: Bool, guarded: Bool) -> IMEStateProtocol
|
func generateStateOfInputting(sansReading: Bool, guarded: Bool) -> IMEStateProtocol
|
||||||
func generateStateOfAssociates(withPair pair: Megrez.KeyValuePaired) -> IMEStateProtocol
|
func generateStateOfAssociates(withPair pair: Megrez.KeyValuePaired) -> IMEStateProtocol
|
||||||
func consolidateNode(
|
func consolidateNode(
|
||||||
|
@ -44,10 +46,6 @@ extension InputHandlerProtocol {
|
||||||
generateStateOfInputting(sansReading: sansReading, guarded: guarded)
|
generateStateOfInputting(sansReading: sansReading, guarded: guarded)
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateStateOfCandidates() -> IMEStateProtocol {
|
|
||||||
generateStateOfCandidates(dodge: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func consolidateNode(candidate: (keyArray: [String], value: String), respectCursorPushing: Bool, preConsolidate: Bool) {
|
func consolidateNode(candidate: (keyArray: [String], value: String), respectCursorPushing: Bool, preConsolidate: Bool) {
|
||||||
consolidateNode(
|
consolidateNode(
|
||||||
candidate: candidate, respectCursorPushing: respectCursorPushing,
|
candidate: candidate, respectCursorPushing: respectCursorPushing,
|
||||||
|
@ -85,20 +83,14 @@ public class InputHandler: InputHandlerProtocol {
|
||||||
public var delegate: InputHandlerDelegate?
|
public var delegate: InputHandlerDelegate?
|
||||||
public var prefs: PrefMgrProtocol
|
public var prefs: PrefMgrProtocol
|
||||||
|
|
||||||
/// 用來記錄「叫出選字窗前」的游標位置的變數。
|
|
||||||
var backupCursor: Int?
|
|
||||||
/// 當前的打字模式。
|
|
||||||
var currentTypingMethod: TypingMethod = .vChewingFactory
|
|
||||||
|
|
||||||
/// 半衰模組的衰減指數
|
/// 半衰模組的衰減指數
|
||||||
let kEpsilon: Double = 0.000_001
|
let kEpsilon: Double = 0.000_001
|
||||||
|
|
||||||
var strCodePointBuffer = "" // 內碼輸入專用組碼區
|
public var calligrapher = "" // 磁帶專用組筆區
|
||||||
var calligrapher = "" // 磁帶專用組筆區
|
public var composer: Tekkon.Composer = .init() // 注拼槽
|
||||||
var composer: Tekkon.Composer = .init() // 注拼槽
|
public var compositor: Megrez.Compositor // 組字器
|
||||||
var compositor: Megrez.Compositor // 組字器
|
public var currentUOM: vChewingLM.LMUserOverride
|
||||||
|
public var currentLM: vChewingLM.LMInstantiator {
|
||||||
public var currentLM: LMAssembly.LMInstantiator {
|
|
||||||
didSet {
|
didSet {
|
||||||
compositor.langModel = .init(withLM: currentLM)
|
compositor.langModel = .init(withLM: currentLM)
|
||||||
clear()
|
clear()
|
||||||
|
@ -106,9 +98,10 @@ public class InputHandler: InputHandlerProtocol {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 初期化。
|
/// 初期化。
|
||||||
public init(lm: LMAssembly.LMInstantiator, pref: PrefMgrProtocol) {
|
public init(lm: vChewingLM.LMInstantiator, uom: vChewingLM.LMUserOverride, pref: PrefMgrProtocol) {
|
||||||
prefs = pref
|
prefs = pref
|
||||||
currentLM = lm
|
currentLM = lm
|
||||||
|
currentUOM = uom
|
||||||
/// 同步組字器單個詞的幅位長度上限。
|
/// 同步組字器單個詞的幅位長度上限。
|
||||||
Megrez.Compositor.maxSpanLength = prefs.maxCandidateLength
|
Megrez.Compositor.maxSpanLength = prefs.maxCandidateLength
|
||||||
/// 組字器初期化。因為是首次初期化變數,所以這裡不能用 ensureCompositor() 代勞。
|
/// 組字器初期化。因為是首次初期化變數,所以這裡不能用 ensureCompositor() 代勞。
|
||||||
|
@ -120,56 +113,48 @@ public class InputHandler: InputHandlerProtocol {
|
||||||
public func clear() {
|
public func clear() {
|
||||||
clearComposerAndCalligrapher()
|
clearComposerAndCalligrapher()
|
||||||
compositor.clear()
|
compositor.clear()
|
||||||
currentTypingMethod = .vChewingFactory
|
isCodePointInputMode = false
|
||||||
backupCursor = nil
|
isHaninKeyboardSymbolMode = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 警告:該參數僅代指組音區/組筆區域與組字區在目前狀態下被視為「空」。
|
/// 警告:該參數僅代指組音區/組筆區域與組字區在目前狀態下被視為「空」。
|
||||||
var isConsideredEmptyForNow: Bool {
|
var isConsideredEmptyForNow: Bool {
|
||||||
compositor.isEmpty && isComposerOrCalligrapherEmpty && currentTypingMethod == .vChewingFactory
|
compositor.isEmpty && isComposerOrCalligrapherEmpty && !isCodePointInputMode && !isHaninKeyboardSymbolMode
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Hanin Keyboard Symbol Mode.
|
||||||
|
|
||||||
|
var isHaninKeyboardSymbolMode = false
|
||||||
|
|
||||||
|
static let tooltipHaninKeyboardSymbolMode: String = "\("Hanin Keyboard Symbol Input.".localized)"
|
||||||
|
|
||||||
|
// MARK: - Codepoint Input Buffer.
|
||||||
|
|
||||||
|
var isCodePointInputMode = false {
|
||||||
|
willSet {
|
||||||
|
strCodePointBuffer.removeAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var strCodePointBuffer = ""
|
||||||
|
|
||||||
|
var tooltipCodePointInputMode: String {
|
||||||
|
let commonTerm = NSMutableString()
|
||||||
|
commonTerm.insert("Code Point Input.".localized, at: 0)
|
||||||
|
if !(delegate?.isVerticalTyping ?? false) {
|
||||||
|
switch IMEApp.currentInputMode {
|
||||||
|
case .imeModeCHS: commonTerm.insert("[GB] ", at: 0)
|
||||||
|
case .imeModeCHT: commonTerm.insert("[Big5] ", at: 0)
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return commonTerm.description
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Functions dealing with Megrez.
|
// MARK: - Functions dealing with Megrez.
|
||||||
|
|
||||||
public var isCompositorEmpty: Bool { compositor.isEmpty }
|
public var isCompositorEmpty: Bool { compositor.isEmpty }
|
||||||
|
|
||||||
func isInvalidEdgeCursorSituation(givenCursor: Int? = nil) -> Bool {
|
|
||||||
let cursorToCheck = givenCursor ?? compositor.cursor
|
|
||||||
// prefs.useRearCursorMode 為 0 (false) 時(macOS 注音選字),最後方的游標位置不合邏輯。
|
|
||||||
// prefs.useRearCursorMode 為 1 (true) 時(微軟新注音選字),最前方的游標位置不合邏輯。
|
|
||||||
switch prefs.useRearCursorMode {
|
|
||||||
case false where cursorToCheck == 0: return true
|
|
||||||
case true where cursorToCheck == compositor.length: return true
|
|
||||||
default: return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func removeBackupCursor() {
|
|
||||||
backupCursor = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
public func dodgeInvalidEdgeCursorForCandidateState() {
|
|
||||||
guard !prefs.useSCPCTypingMode else { return }
|
|
||||||
guard prefs.dodgeInvalidEdgeCandidateCursorPosition else { return }
|
|
||||||
guard isInvalidEdgeCursorSituation() else { return }
|
|
||||||
backupCursor = compositor.cursor
|
|
||||||
switch prefs.useRearCursorMode {
|
|
||||||
case false where compositor.cursor < compositor.length:
|
|
||||||
compositor.cursor += 1
|
|
||||||
if isCursorCuttingChar() { compositor.jumpCursorBySpan(to: .front) }
|
|
||||||
case true where compositor.cursor > 0:
|
|
||||||
compositor.cursor -= 1
|
|
||||||
if isCursorCuttingChar() { compositor.jumpCursorBySpan(to: .rear) }
|
|
||||||
default: break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func restoreBackupCursor() {
|
|
||||||
guard let theBackupCursor = backupCursor else { return }
|
|
||||||
compositor.cursor = Swift.max(Swift.min(theBackupCursor, compositor.length), 0)
|
|
||||||
backupCursor = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 獲取當前標記得範圍。這個函式只能是函式、而非只讀變數。
|
/// 獲取當前標記得範圍。這個函式只能是函式、而非只讀變數。
|
||||||
/// - Returns: 當前標記範圍。
|
/// - Returns: 當前標記範圍。
|
||||||
func currentMarkedRange() -> Range<Int> {
|
func currentMarkedRange() -> Range<Int> {
|
||||||
|
@ -366,8 +351,8 @@ public class InputHandler: InputHandlerProtocol {
|
||||||
let currentNode = currentWalk.findNode(at: actualNodeCursorPosition, target: &accumulatedCursor)
|
let currentNode = currentWalk.findNode(at: actualNodeCursorPosition, target: &accumulatedCursor)
|
||||||
guard let currentNode = currentNode else { return }
|
guard let currentNode = currentNode else { return }
|
||||||
|
|
||||||
uomProcessing: if currentNode.currentUnigram.score > -12, prefs.fetchSuggestionsFromUserOverrideModel {
|
uom: if currentNode.currentUnigram.score > -12, prefs.fetchSuggestionsFromUserOverrideModel {
|
||||||
if skipObservation { break uomProcessing }
|
if skipObservation { break uom }
|
||||||
vCLog("UOM: Start Observation.")
|
vCLog("UOM: Start Observation.")
|
||||||
// 這個過程可能會因為使用者半衰記憶模組內部資料錯亂、而導致輸入法在選字時崩潰。
|
// 這個過程可能會因為使用者半衰記憶模組內部資料錯亂、而導致輸入法在選字時崩潰。
|
||||||
// 於是在這裡引入災後狀況察覺專用變數,且先開啟該開關。順利執行完觀察後會關閉。
|
// 於是在這裡引入災後狀況察覺專用變數,且先開啟該開關。順利執行完觀察後會關閉。
|
||||||
|
@ -375,9 +360,9 @@ public class InputHandler: InputHandlerProtocol {
|
||||||
prefs.failureFlagForUOMObservation = true
|
prefs.failureFlagForUOMObservation = true
|
||||||
// 令半衰記憶模組觀測給定的三元圖。
|
// 令半衰記憶模組觀測給定的三元圖。
|
||||||
// 這個過程會讓半衰引擎根據當前上下文生成三元圖索引鍵。
|
// 這個過程會讓半衰引擎根據當前上下文生成三元圖索引鍵。
|
||||||
currentLM.performUOMObservation(
|
currentUOM.performObservation(
|
||||||
walkedBefore: previousWalk, walkedAfter: currentWalk, cursor: actualNodeCursorPosition,
|
walkedBefore: previousWalk, walkedAfter: currentWalk, cursor: actualNodeCursorPosition,
|
||||||
timestamp: Date().timeIntervalSince1970
|
timestamp: Date().timeIntervalSince1970, saveCallback: { self.currentUOM.saveData() }
|
||||||
)
|
)
|
||||||
// 如果沒有出現崩框的話,那就將這個開關復位。
|
// 如果沒有出現崩框的話,那就將這個開關復位。
|
||||||
prefs.failureFlagForUOMObservation = false
|
prefs.failureFlagForUOMObservation = false
|
||||||
|
@ -429,7 +414,7 @@ public class InputHandler: InputHandlerProtocol {
|
||||||
/// 如果這個開關沒打開的話,直接放棄執行這個函式。
|
/// 如果這個開關沒打開的話,直接放棄執行這個函式。
|
||||||
if !prefs.fetchSuggestionsFromUserOverrideModel { return arrResult }
|
if !prefs.fetchSuggestionsFromUserOverrideModel { return arrResult }
|
||||||
/// 獲取來自半衰記憶模組的建議結果
|
/// 獲取來自半衰記憶模組的建議結果
|
||||||
let suggestion = currentLM.fetchUOMSuggestion(
|
let suggestion = currentUOM.fetchSuggestion(
|
||||||
currentWalk: compositor.walkedNodes, cursor: actualNodeCursorPosition, timestamp: Date().timeIntervalSince1970
|
currentWalk: compositor.walkedNodes, cursor: actualNodeCursorPosition, timestamp: Date().timeIntervalSince1970
|
||||||
)
|
)
|
||||||
arrResult.append(contentsOf: suggestion.candidates)
|
arrResult.append(contentsOf: suggestion.candidates)
|
|
@ -9,9 +9,9 @@
|
||||||
/// 該檔案乃輸入調度模組當中「用來規定在選字窗出現時的按鍵行為」的部分。
|
/// 該檔案乃輸入調度模組當中「用來規定在選字窗出現時的按鍵行為」的部分。
|
||||||
|
|
||||||
import CandidateWindow
|
import CandidateWindow
|
||||||
|
import CocoaExtension
|
||||||
import InputMethodKit
|
import InputMethodKit
|
||||||
import Megrez
|
import Megrez
|
||||||
import OSFrameworkImpl
|
|
||||||
import Shared
|
import Shared
|
||||||
|
|
||||||
// MARK: - § 對選字狀態進行調度 (Handle Candidate State).
|
// MARK: - § 對選字狀態進行調度 (Handle Candidate State).
|
||||||
|
@ -29,48 +29,6 @@ extension InputHandler {
|
||||||
guard state.isCandidateContainer else { return false } // 會自動判斷「isEmpty」。
|
guard state.isCandidateContainer else { return false } // 會自動判斷「isEmpty」。
|
||||||
guard ctlCandidate.visible else { return false }
|
guard ctlCandidate.visible else { return false }
|
||||||
let inputText = ignoringModifiers ? (input.inputTextIgnoringModifiers ?? input.text) : input.text
|
let inputText = ignoringModifiers ? (input.inputTextIgnoringModifiers ?? input.text) : input.text
|
||||||
let allowMovingCompositorCursor = state.type == .ofCandidates && !prefs.useSCPCTypingMode
|
|
||||||
let highlightedCandidate = state.candidates[ctlCandidate.highlightedIndex]
|
|
||||||
|
|
||||||
// MARK: 選字窗服務選單(Shift+?)。
|
|
||||||
|
|
||||||
var candidateTextServiceMenuRunning: Bool {
|
|
||||||
state.node.containsCandidateServices && state.type == .ofSymbolTable
|
|
||||||
}
|
|
||||||
|
|
||||||
serviceMenu: if prefs.useShiftQuestionToCallServiceMenu, input.commonKeyModifierFlags == .shift, input.text == "?" {
|
|
||||||
if candidateTextServiceMenuRunning { break serviceMenu }
|
|
||||||
let handled = handleServiceMenuInitiation(
|
|
||||||
candidateText: highlightedCandidate.value,
|
|
||||||
reading: highlightedCandidate.keyArray
|
|
||||||
)
|
|
||||||
if handled { return true }
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: 波浪符號鍵(選字窗服務選單 / 輔助翻頁 / 其他功能)。
|
|
||||||
|
|
||||||
if input.isSymbolMenuPhysicalKey {
|
|
||||||
switch input.commonKeyModifierFlags {
|
|
||||||
case .shift, [],
|
|
||||||
.option where !candidateTextServiceMenuRunning:
|
|
||||||
if !candidateTextServiceMenuRunning {
|
|
||||||
let handled = handleServiceMenuInitiation(
|
|
||||||
candidateText: highlightedCandidate.value,
|
|
||||||
reading: highlightedCandidate.keyArray
|
|
||||||
)
|
|
||||||
if handled { return true }
|
|
||||||
}
|
|
||||||
var updated = true
|
|
||||||
let reverseTrigger = input.isShiftHold || input.isOptionHold
|
|
||||||
updated = reverseTrigger ? ctlCandidate.showPreviousLine() : ctlCandidate.showNextLine()
|
|
||||||
if !updated { delegate.callError("66F3477B") }
|
|
||||||
return true
|
|
||||||
case .option where state.type == .ofSymbolTable:
|
|
||||||
// 繞過內碼輸入模式,直接進入漢音鍵盤符號模式。
|
|
||||||
return revolveTypingMethod(to: .haninKeyboardSymbol)
|
|
||||||
default: break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: 選字窗內使用熱鍵升權、降權、刪詞。
|
// MARK: 選字窗內使用熱鍵升權、降權、刪詞。
|
||||||
|
|
||||||
|
@ -142,6 +100,7 @@ extension InputHandler {
|
||||||
delegate.switchState(IMEState.ofAbortion())
|
delegate.switchState(IMEState.ofAbortion())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
let highlightedCandidate = state.candidates[ctlCandidate.highlightedIndex] // 關聯詞語功能專用。
|
||||||
var handleAssociates = !prefs.useSCPCTypingMode && prefs.associatedPhrasesEnabled // 關聯詞語功能專用。
|
var handleAssociates = !prefs.useSCPCTypingMode && prefs.associatedPhrasesEnabled // 關聯詞語功能專用。
|
||||||
handleAssociates = handleAssociates && compositor.cursor == compositor.length // 關聯詞語功能專用。
|
handleAssociates = handleAssociates && compositor.cursor == compositor.length // 關聯詞語功能專用。
|
||||||
confirmHighlightedCandidate()
|
confirmHighlightedCandidate()
|
||||||
|
@ -192,7 +151,7 @@ extension InputHandler {
|
||||||
return true
|
return true
|
||||||
case .kUpArrow, .kDownArrow, .kLeftArrow, .kRightArrow:
|
case .kUpArrow, .kDownArrow, .kLeftArrow, .kRightArrow:
|
||||||
switch input.commonKeyModifierFlags {
|
switch input.commonKeyModifierFlags {
|
||||||
case [.option, .shift] where allowMovingCompositorCursor && input.isCursorForward:
|
case [.option, .shift] where input.isCursorForward:
|
||||||
if compositor.cursor < compositor.length {
|
if compositor.cursor < compositor.length {
|
||||||
compositor.cursor += 1
|
compositor.cursor += 1
|
||||||
if isCursorCuttingChar() { compositor.jumpCursorBySpan(to: .front) }
|
if isCursorCuttingChar() { compositor.jumpCursorBySpan(to: .front) }
|
||||||
|
@ -201,7 +160,7 @@ extension InputHandler {
|
||||||
delegate.callError("D3006C85")
|
delegate.callError("D3006C85")
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
case [.option, .shift] where allowMovingCompositorCursor && input.isCursorBackward:
|
case [.option, .shift] where input.isCursorBackward:
|
||||||
if compositor.cursor > 0 {
|
if compositor.cursor > 0 {
|
||||||
compositor.cursor -= 1
|
compositor.cursor -= 1
|
||||||
if isCursorCuttingChar() { compositor.jumpCursorBySpan(to: .rear) }
|
if isCursorCuttingChar() { compositor.jumpCursorBySpan(to: .rear) }
|
||||||
|
@ -255,55 +214,22 @@ extension InputHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: J / K 鍵組字區的游標移動行為處理
|
|
||||||
|
|
||||||
let allowMovingCompositorCursorByJK = allowMovingCompositorCursor && prefs.useJKtoMoveCompositorCursorInCandidateState
|
|
||||||
|
|
||||||
checkMovingCompositorCursorByJK: if allowMovingCompositorCursorByJK {
|
|
||||||
guard input.keyModifierFlags.isEmpty else { break checkMovingCompositorCursorByJK }
|
|
||||||
// keycode: 38 = J, 40 = K.
|
|
||||||
switch input.keyCode {
|
|
||||||
case 38:
|
|
||||||
if compositor.cursor > 0 {
|
|
||||||
compositor.cursor -= 1
|
|
||||||
if isCursorCuttingChar() { compositor.jumpCursorBySpan(to: .rear) }
|
|
||||||
delegate.switchState(generateStateOfCandidates())
|
|
||||||
} else {
|
|
||||||
delegate.callError("6F389AE9")
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
case 40:
|
|
||||||
if compositor.cursor < compositor.length {
|
|
||||||
compositor.cursor += 1
|
|
||||||
if isCursorCuttingChar() { compositor.jumpCursorBySpan(to: .front) }
|
|
||||||
delegate.switchState(generateStateOfCandidates())
|
|
||||||
} else {
|
|
||||||
delegate.callError("EDBD27F2")
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
default: break checkMovingCompositorCursorByJK
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: 關聯詞語處理 (Associated Phrases) 以及標準選字處理
|
// MARK: 關聯詞語處理 (Associated Phrases) 以及標準選字處理
|
||||||
|
|
||||||
if state.type == .ofAssociates, !input.isShiftHold { return false }
|
if state.type == .ofAssociates, !input.isShiftHold { return false }
|
||||||
|
|
||||||
var index: Int?
|
var index: Int?
|
||||||
var shaltShiftHold = [.ofAssociates].contains(state.type)
|
var shaltShiftHold = [.ofAssociates].contains(state.type)
|
||||||
if state.type == .ofInputting {
|
if [.ofInputting].contains(state.type) {
|
||||||
let cassetteShift = currentLM.areCassetteCandidateKeysShiftHeld
|
let cassetteShift = currentLM.areCassetteCandidateKeysShiftHeld
|
||||||
shaltShiftHold = shaltShiftHold || cassetteShift
|
shaltShiftHold = shaltShiftHold || cassetteShift
|
||||||
}
|
}
|
||||||
let matched: String = (shaltShiftHold ? input.inputTextIgnoringModifiers ?? "" : inputText).lowercased()
|
let matched: String = shaltShiftHold ? input.inputTextIgnoringModifiers ?? "" : inputText
|
||||||
// 如果允許 J / K 鍵前後移動組字區游標的話,則不再將 J / K 鍵盤視為選字鍵。
|
|
||||||
if !(prefs.useJKtoMoveCompositorCursorInCandidateState && "jk".contains(matched)) {
|
|
||||||
checkSelectionKey: for keyPair in delegate.selectionKeys.enumerated() {
|
checkSelectionKey: for keyPair in delegate.selectionKeys.enumerated() {
|
||||||
guard matched == keyPair.element.lowercased() else { continue }
|
guard matched.lowercased() == keyPair.element.lowercased() else { continue }
|
||||||
index = Int(keyPair.offset)
|
index = Int(keyPair.offset)
|
||||||
break checkSelectionKey
|
break checkSelectionKey
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 標準選字處理
|
// 標準選字處理
|
||||||
if let index = index, let candidateIndex = ctlCandidate.candidateIndexAtKeyLabelIndex(index) {
|
if let index = index, let candidateIndex = ctlCandidate.candidateIndexAtKeyLabelIndex(index) {
|
||||||
|
@ -355,7 +281,7 @@ extension InputHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Flipping pages by using modified bracket keys (when they are not occupied).
|
// MARK: - Flipping pages by using modified bracket keys (when they are not occupied).
|
||||||
|
|
||||||
// Shift+Command+[] 被 Chrome 系瀏覽器佔用,所以改用 Ctrl。
|
// Shift+Command+[] 被 Chrome 系瀏覽器佔用,所以改用 Ctrl。
|
||||||
let ctrlCMD: Bool = input.commonKeyModifierFlags == [.control, .command]
|
let ctrlCMD: Bool = input.commonKeyModifierFlags == [.control, .command]
|
||||||
|
@ -373,6 +299,23 @@ 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 handleHaninKeyboardSymbolModeToggle()
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if state.type == .ofInputting { return false } // `%quick`
|
if state.type == .ofInputting { return false } // `%quick`
|
||||||
|
|
||||||
delegate.callError("172A0F81")
|
delegate.callError("172A0F81")
|
|
@ -17,29 +17,37 @@ extension InputHandler {
|
||||||
/// - Parameter input: 輸入訊號。
|
/// - Parameter input: 輸入訊號。
|
||||||
/// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。
|
/// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。
|
||||||
func handleComposition(input: InputSignalProtocol) -> Bool? {
|
func handleComposition(input: InputSignalProtocol) -> Bool? {
|
||||||
|
guard let delegate = delegate else { return nil }
|
||||||
// 不處理任何包含不可列印字元的訊號。
|
// 不處理任何包含不可列印字元的訊號。
|
||||||
let hardRequirementMet = !input.text.isEmpty && input.charCode.isPrintable
|
guard !input.text.isEmpty, input.charCode.isPrintable else { return nil }
|
||||||
switch currentTypingMethod {
|
if isCodePointInputMode { return handleCodePointComposition(input: input) }
|
||||||
case .codePoint where hardRequirementMet:
|
if prefs.cassetteEnabled {
|
||||||
return handleCodePointComposition(input: input)
|
// 準備處理 `%quick` 選字行為。
|
||||||
case .haninKeyboardSymbol where [[], .shift].contains(input.keyModifierFlags):
|
var handleQuickCandidate = true
|
||||||
return handleHaninKeyboardSymbolModeInput(input: input)
|
if currentLM.areCassetteCandidateKeysShiftHeld { handleQuickCandidate = input.isShiftHold }
|
||||||
case .vChewingFactory where hardRequirementMet && prefs.cassetteEnabled:
|
let hasQuickCandidates: Bool = delegate.state.type == .ofInputting && delegate.state.isCandidateContainer
|
||||||
|
|
||||||
|
// 處理 `%symboldef` 選字行為。
|
||||||
|
if handleCassetteSymbolTable(input: input) {
|
||||||
|
return true
|
||||||
|
} else if hasQuickCandidates, input.text != currentLM.cassetteWildcardKey {
|
||||||
|
// 處理 `%quick` 選字行為(當且僅當與 `%symboldef` 衝突的情況下)。
|
||||||
|
guard !(handleQuickCandidate && handleCandidate(input: input, ignoringModifiers: true)) else { return true }
|
||||||
|
} else {
|
||||||
|
// 處理 `%quick` 選字行為。
|
||||||
|
guard !(hasQuickCandidates && handleQuickCandidate && handleCandidate(input: input)) else { return true }
|
||||||
|
}
|
||||||
return handleCassetteComposition(input: input)
|
return handleCassetteComposition(input: input)
|
||||||
case .vChewingFactory where hardRequirementMet && !prefs.cassetteEnabled:
|
}
|
||||||
return handlePhonabetComposition(input: input)
|
return handlePhonabetComposition(input: input)
|
||||||
default: return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 注音按鍵輸入處理 (Handle BPMF Keys)
|
// MARK: 注音按鍵輸入處理 (Handle BPMF Keys)
|
||||||
|
|
||||||
private extension InputHandler {
|
|
||||||
/// 用來處理 InputHandler.HandleInput() 當中的與注音输入有關的組字行為。
|
/// 用來處理 InputHandler.HandleInput() 當中的與注音输入有關的組字行為。
|
||||||
/// - Parameter input: 輸入訊號。
|
/// - Parameter input: 輸入訊號。
|
||||||
/// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。
|
/// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。
|
||||||
func handlePhonabetComposition(input: InputSignalProtocol) -> Bool? {
|
private func handlePhonabetComposition(input: InputSignalProtocol) -> Bool? {
|
||||||
guard let delegate = delegate else { return nil }
|
guard let delegate = delegate else { return nil }
|
||||||
var inputText = (input.inputTextIgnoringModifiers ?? input.text)
|
var inputText = (input.inputTextIgnoringModifiers ?? input.text)
|
||||||
inputText = inputText.lowercased().applyingTransformFW2HW(reverse: false)
|
inputText = inputText.lowercased().applyingTransformFW2HW(reverse: false)
|
||||||
|
@ -58,7 +66,7 @@ private extension InputHandler {
|
||||||
|
|
||||||
func narrateTheComposer(with maybeKey: String? = nil, when condition: Bool, allowDuplicates: Bool = true) {
|
func narrateTheComposer(with maybeKey: String? = nil, when condition: Bool, allowDuplicates: Bool = true) {
|
||||||
guard condition else { return }
|
guard condition else { return }
|
||||||
let maybeKey = maybeKey ?? composer.phonabetKeyForQuery(pronounceableOnly: prefs.acceptLeadingIntonations)
|
let maybeKey = maybeKey ?? composer.phonabetKeyForQuery(pronouncable: prefs.acceptLeadingIntonations)
|
||||||
guard var keyToNarrate = maybeKey else { return }
|
guard var keyToNarrate = maybeKey else { return }
|
||||||
if composer.intonation == Tekkon.Phonabet(" ") { keyToNarrate.append("ˉ") }
|
if composer.intonation == Tekkon.Phonabet(" ") { keyToNarrate.append("ˉ") }
|
||||||
SpeechSputnik.shared.narrate(keyToNarrate, allowDuplicates: allowDuplicates)
|
SpeechSputnik.shared.narrate(keyToNarrate, allowDuplicates: allowDuplicates)
|
||||||
|
@ -119,7 +127,7 @@ private extension InputHandler {
|
||||||
return handleEnter(input: input, readingOnly: true)
|
return handleEnter(input: input, readingOnly: true)
|
||||||
}
|
}
|
||||||
// 拿取用來進行索引檢索用的注音。這裡先不急著處理「僅有注音符號輸入」的情況。
|
// 拿取用來進行索引檢索用的注音。這裡先不急著處理「僅有注音符號輸入」的情況。
|
||||||
let maybeKey = composer.phonabetKeyForQuery(pronounceableOnly: prefs.acceptLeadingIntonations)
|
let maybeKey = composer.phonabetKeyForQuery(pronouncable: prefs.acceptLeadingIntonations)
|
||||||
guard let readingKey = maybeKey else { break ifComposeReading }
|
guard let readingKey = maybeKey else { break ifComposeReading }
|
||||||
// 向語言模型詢問是否有對應的記錄。
|
// 向語言模型詢問是否有對應的記錄。
|
||||||
if !currentLM.hasUnigramsFor(keyArray: [readingKey]) {
|
if !currentLM.hasUnigramsFor(keyArray: [readingKey]) {
|
||||||
|
@ -204,7 +212,7 @@ private extension InputHandler {
|
||||||
/// 但這裡不處理陰平聲調。
|
/// 但這裡不處理陰平聲調。
|
||||||
if keyConsumedByReading {
|
if keyConsumedByReading {
|
||||||
// 此處將 strict 設為 false,以應對「僅有注音符號輸入」的情況。
|
// 此處將 strict 設為 false,以應對「僅有注音符號輸入」的情況。
|
||||||
if composer.phonabetKeyForQuery(pronounceableOnly: false) == nil {
|
if composer.phonabetKeyForQuery(pronouncable: false) == nil {
|
||||||
// 將被空格鍵覆蓋掉的既有聲調塞入組字器。
|
// 將被空格鍵覆蓋掉的既有聲調塞入組字器。
|
||||||
if !composer.isPinyinMode, input.isSpace,
|
if !composer.isPinyinMode, input.isSpace,
|
||||||
compositor.insertKey(existedIntonation.value)
|
compositor.insertKey(existedIntonation.value)
|
||||||
|
@ -233,31 +241,13 @@ private extension InputHandler {
|
||||||
|
|
||||||
// MARK: - 磁帶模式的組字支援。
|
// MARK: - 磁帶模式的組字支援。
|
||||||
|
|
||||||
private extension InputHandler {
|
extension InputHandler {
|
||||||
/// 用來處理 InputHandler.HandleInput() 當中的與磁帶模組有關的組字行為。(前置處理)
|
/// 用來處理 InputHandler.HandleInput() 當中的與磁帶模組有關的組字行為。
|
||||||
/// - Parameter input: 輸入訊號。
|
/// - Parameter input: 輸入訊號。
|
||||||
/// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。
|
/// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。
|
||||||
func handleCassetteComposition(input: InputSignalProtocol) -> Bool? {
|
private func handleCassetteComposition(input: InputSignalProtocol) -> Bool? {
|
||||||
guard let delegate = delegate else { return nil }
|
guard let delegate = delegate else { return nil }
|
||||||
let state = delegate.state
|
let state = delegate.state
|
||||||
|
|
||||||
// 準備處理 `%quick` 選字行為。
|
|
||||||
var handleQuickCandidate = true
|
|
||||||
if currentLM.areCassetteCandidateKeysShiftHeld { handleQuickCandidate = input.isShiftHold }
|
|
||||||
let hasQuickCandidates: Bool = state.type == .ofInputting && state.isCandidateContainer
|
|
||||||
|
|
||||||
// 處理 `%symboldef` 選字行為。
|
|
||||||
if handleCassetteSymbolTable(input: input) {
|
|
||||||
return true
|
|
||||||
} else if hasQuickCandidates, input.text != currentLM.cassetteWildcardKey {
|
|
||||||
// 處理 `%quick` 選字行為(當且僅當與 `%symboldef` 衝突的情況下)。
|
|
||||||
guard !(handleQuickCandidate && handleCandidate(input: input, ignoringModifiers: true)) else { return true }
|
|
||||||
} else {
|
|
||||||
// 處理 `%quick` 選字行為。
|
|
||||||
guard !(hasQuickCandidates && handleQuickCandidate && handleCandidate(input: input)) else { return true }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 正式處理。
|
|
||||||
var wildcardKey: String { currentLM.cassetteWildcardKey } // 花牌鍵。
|
var wildcardKey: String { currentLM.cassetteWildcardKey } // 花牌鍵。
|
||||||
let inputText = input.text
|
let inputText = input.text
|
||||||
let isWildcardKeyInput: Bool = (inputText == wildcardKey && !wildcardKey.isEmpty)
|
let isWildcardKeyInput: Bool = (inputText == wildcardKey && !wildcardKey.isEmpty)
|
||||||
|
@ -395,17 +385,16 @@ private extension InputHandler {
|
||||||
// 將「這個按鍵訊號已經被輸入法攔截處理了」的結果藉由 SessionCtl 回報給 IMK。
|
// 將「這個按鍵訊號已經被輸入法攔截處理了」的結果藉由 SessionCtl 回報給 IMK。
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 內碼區位輸入處理 (Handle Code Point Input)
|
// MARK: 內碼區位輸入處理 (Handle Code Point Input)
|
||||||
|
|
||||||
private extension InputHandler {
|
|
||||||
/// 用來處理 InputHandler.HandleInput() 當中的與內碼區位輸入有關的組字行為。
|
/// 用來處理 InputHandler.HandleInput() 當中的與內碼區位輸入有關的組字行為。
|
||||||
/// - Parameter input: 輸入訊號。
|
/// - Parameter input: 輸入訊號。
|
||||||
/// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。
|
/// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。
|
||||||
func handleCodePointComposition(input: InputSignalProtocol) -> Bool? {
|
private func handleCodePointComposition(input: InputSignalProtocol) -> Bool? {
|
||||||
guard !input.isReservedKey else { return nil }
|
guard !input.isReservedKey else { return nil }
|
||||||
guard let delegate = delegate, input.text.count == 1 else { return nil }
|
guard let delegate = delegate, input.text.count == 1 else { return nil }
|
||||||
guard !input.text.compactMap(\.hexDigitValue).isEmpty else {
|
guard !input.text.compactMap(\.hexDigitValue).isEmpty else {
|
||||||
|
@ -418,20 +407,27 @@ private extension InputHandler {
|
||||||
strCodePointBuffer.append(input.text)
|
strCodePointBuffer.append(input.text)
|
||||||
var updatedState = generateStateOfInputting(guarded: true)
|
var updatedState = generateStateOfInputting(guarded: true)
|
||||||
updatedState.tooltipDuration = 0
|
updatedState.tooltipDuration = 0
|
||||||
updatedState.tooltip = TypingMethod.codePoint.getTooltip(vertical: delegate.isVerticalTyping)
|
updatedState.tooltip = tooltipCodePointInputMode
|
||||||
delegate.switchState(updatedState)
|
delegate.switchState(updatedState)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
let encoding: CFStringEncodings? = {
|
||||||
|
switch IMEApp.currentInputMode {
|
||||||
|
case .imeModeCHS: return .GB_18030_2000
|
||||||
|
case .imeModeCHT: return .big5_HKSCS_1999
|
||||||
|
default: return nil
|
||||||
|
}
|
||||||
|
}()
|
||||||
guard
|
guard
|
||||||
var char = "\(strCodePointBuffer)\(input.text)"
|
var char = "\(strCodePointBuffer)\(input.text)"
|
||||||
.parsedAsHexLiteral(encoding: IMEApp.currentInputMode.nonUTFEncoding)?.first?.description
|
.parsedAsHexLiteral(encoding: encoding)?.first?.description
|
||||||
else {
|
else {
|
||||||
delegate.callError("D220B880:輸入的字碼沒有對應的字元。")
|
delegate.callError("D220B880:輸入的字碼沒有對應的字元。")
|
||||||
var updatedState = IMEState.ofAbortion()
|
var updatedState = IMEState.ofAbortion()
|
||||||
updatedState.tooltipDuration = 0
|
updatedState.tooltipDuration = 0
|
||||||
updatedState.tooltip = "Invalid Code Point.".localized
|
updatedState.tooltip = "Invalid Code Point.".localized
|
||||||
delegate.switchState(updatedState)
|
delegate.switchState(updatedState)
|
||||||
currentTypingMethod = .codePoint
|
isCodePointInputMode = true
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// 某些舊版 macOS 會在這裡生成的字元後面插入垃圾字元。這裡只保留起始字元。
|
// 某些舊版 macOS 會在這裡生成的字元後面插入垃圾字元。這裡只保留起始字元。
|
||||||
|
@ -439,46 +435,14 @@ private extension InputHandler {
|
||||||
delegate.switchState(IMEState.ofCommitting(textToCommit: char))
|
delegate.switchState(IMEState.ofCommitting(textToCommit: char))
|
||||||
var updatedState = generateStateOfInputting(guarded: true)
|
var updatedState = generateStateOfInputting(guarded: true)
|
||||||
updatedState.tooltipDuration = 0
|
updatedState.tooltipDuration = 0
|
||||||
updatedState.tooltip = TypingMethod.codePoint.getTooltip(vertical: delegate.isVerticalTyping)
|
updatedState.tooltip = tooltipCodePointInputMode
|
||||||
delegate.switchState(updatedState)
|
delegate.switchState(updatedState)
|
||||||
currentTypingMethod = .codePoint
|
isCodePointInputMode = true
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
delegate.switchState(generateStateOfInputting())
|
delegate.switchState(generateStateOfInputting())
|
||||||
currentTypingMethod = .codePoint
|
isCodePointInputMode = true
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 處理漢音鍵盤符號輸入狀態(Handle Hanin Keyboard Symbol Inputs)
|
|
||||||
|
|
||||||
private extension InputHandler {
|
|
||||||
/// 處理漢音鍵盤符號輸入。
|
|
||||||
/// - Parameters:
|
|
||||||
/// - input: 輸入按鍵訊號。
|
|
||||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 SessionCtl 回報給 IMK。
|
|
||||||
func handleHaninKeyboardSymbolModeInput(input: InputSignalProtocol) -> Bool {
|
|
||||||
guard let delegate = delegate, delegate.state.type != .ofDeactivated else { return false }
|
|
||||||
let charText = input.text.lowercased().applyingTransformFW2HW(reverse: false)
|
|
||||||
guard CandidateNode.mapHaninKeyboardSymbols.keys.contains(charText) else {
|
|
||||||
return revolveTypingMethod(to: .vChewingFactory)
|
|
||||||
}
|
|
||||||
guard
|
|
||||||
charText.count == 1, let symbols = CandidateNode.queryHaninKeyboardSymbols(char: charText)
|
|
||||||
else {
|
|
||||||
delegate.callError("C1A760C7")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// 得在這裡先 commit buffer,不然會導致「在摁 ESC 離開符號選單時會重複輸入上一次的組字區的內容」的不當行為。
|
|
||||||
let textToCommit = generateStateOfInputting(sansReading: true).displayedText
|
|
||||||
delegate.switchState(IMEState.ofCommitting(textToCommit: textToCommit))
|
|
||||||
if symbols.members.count == 1 {
|
|
||||||
delegate.switchState(IMEState.ofCommitting(textToCommit: symbols.members.map(\.name).joined()))
|
|
||||||
} else {
|
|
||||||
delegate.switchState(IMEState.ofSymbolTable(node: symbols))
|
|
||||||
}
|
|
||||||
currentTypingMethod = .vChewingFactory // 用完就關掉,但保持選字窗開啟,所以這裡不用呼叫 toggle 函式。
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -25,20 +25,18 @@ extension InputHandler {
|
||||||
/// - Returns: 生成的「正在輸入」狀態。
|
/// - Returns: 生成的「正在輸入」狀態。
|
||||||
public func generateStateOfInputting(sansReading: Bool = false, guarded: Bool = false) -> IMEStateProtocol {
|
public func generateStateOfInputting(sansReading: Bool = false, guarded: Bool = false) -> IMEStateProtocol {
|
||||||
if isConsideredEmptyForNow, !guarded { return IMEState.ofAbortion() }
|
if isConsideredEmptyForNow, !guarded { return IMEState.ofAbortion() }
|
||||||
restoreBackupCursor() // 只要叫了 Inputting 狀態,就盡可能還原游標備份。
|
|
||||||
var segHighlightedAt: Int?
|
var segHighlightedAt: Int?
|
||||||
let handleAsCodePointInput = currentTypingMethod == .codePoint && !sansReading
|
let cpInput = isCodePointInputMode && !sansReading
|
||||||
/// 「更新內文組字區 (Update the composing buffer)」是指要求客體軟體將組字緩衝區的內容
|
/// 「更新內文組字區 (Update the composing buffer)」是指要求客體軟體將組字緩衝區的內容
|
||||||
/// 換成由此處重新生成的原始資料在 IMEStateData 當中生成的 NSAttributeString。
|
/// 換成由此處重新生成的原始資料在 IMEStateData 當中生成的 NSAttributeString。
|
||||||
var displayTextSegments: [String] = handleAsCodePointInput
|
var displayTextSegments: [String] = cpInput
|
||||||
? [strCodePointBuffer]
|
? [strCodePointBuffer]
|
||||||
: compositor.walkedNodes.values
|
: compositor.walkedNodes.values
|
||||||
var cursor = handleAsCodePointInput
|
var cursor = cpInput
|
||||||
? displayTextSegments.joined().count
|
? displayTextSegments.joined().count
|
||||||
: convertCursorForDisplay(compositor.cursor)
|
: convertCursorForDisplay(compositor.cursor)
|
||||||
let cursorSansReading = cursor
|
let cursorSansReading = cursor
|
||||||
// 先提出來讀音資料,減輕運算負擔。
|
let reading: String = (sansReading || isCodePointInputMode) ? "" : readingForDisplay // 先提出來,減輕運算負擔。
|
||||||
let reading: String = (sansReading || currentTypingMethod == .codePoint) ? "" : readingForDisplay
|
|
||||||
if !reading.isEmpty {
|
if !reading.isEmpty {
|
||||||
var newDisplayTextSegments = [String]()
|
var newDisplayTextSegments = [String]()
|
||||||
var temporaryNode = ""
|
var temporaryNode = ""
|
||||||
|
@ -136,15 +134,11 @@ extension InputHandler {
|
||||||
|
|
||||||
/// 拿著給定的候選字詞陣列資料內容,切換至選字狀態。
|
/// 拿著給定的候選字詞陣列資料內容,切換至選字狀態。
|
||||||
/// - Returns: 回呼一個新的選詞狀態,來就給定的候選字詞陣列資料內容顯示選字窗。
|
/// - Returns: 回呼一個新的選詞狀態,來就給定的候選字詞陣列資料內容顯示選字窗。
|
||||||
public func generateStateOfCandidates(dodge: Bool = true) -> IMEStateProtocol {
|
public func generateStateOfCandidates() -> IMEStateProtocol {
|
||||||
guard let delegate = delegate else { return IMEState.ofAbortion() }
|
|
||||||
if dodge, delegate.state.type == .ofInputting {
|
|
||||||
dodgeInvalidEdgeCursorForCandidateState()
|
|
||||||
}
|
|
||||||
var result = IMEState.ofCandidates(
|
var result = IMEState.ofCandidates(
|
||||||
candidates: generateArrayOfCandidates(fixOrder: prefs.useFixedCandidateOrderOnSelection),
|
candidates: generateArrayOfCandidates(fixOrder: prefs.useFixedCandidateOrderOnSelection),
|
||||||
displayTextSegments: compositor.walkedNodes.values,
|
displayTextSegments: compositor.walkedNodes.values,
|
||||||
cursor: compositor.cursor
|
cursor: delegate?.state.cursor ?? generateStateOfInputting().cursor
|
||||||
)
|
)
|
||||||
if !prefs.useRearCursorMode {
|
if !prefs.useRearCursorMode {
|
||||||
let markerBackup = compositor.marker
|
let markerBackup = compositor.marker
|
||||||
|
@ -362,9 +356,8 @@ extension InputHandler {
|
||||||
guard let delegate = delegate else { return false }
|
guard let delegate = delegate else { return false }
|
||||||
let state = delegate.state
|
let state = delegate.state
|
||||||
|
|
||||||
guard currentTypingMethod == .vChewingFactory else {
|
if isHaninKeyboardSymbolMode { return handleHaninKeyboardSymbolModeToggle() }
|
||||||
return revolveTypingMethod(to: .vChewingFactory)
|
if isCodePointInputMode { return handleCodePointInputToggle() }
|
||||||
}
|
|
||||||
|
|
||||||
guard state.type == .ofInputting else { return false }
|
guard state.type == .ofInputting else { return false }
|
||||||
|
|
||||||
|
@ -393,6 +386,75 @@ extension InputHandler {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Command+Enter 鍵的處理(注音文)
|
||||||
|
|
||||||
|
/// Command+Enter 鍵的處理(注音文)。
|
||||||
|
/// - Parameter isShiftPressed: 有沒有同時摁著 Shift 鍵。
|
||||||
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 SessionCtl 回報給 IMK。
|
||||||
|
private func commissionByCtrlCommandEnter(isShiftPressed: Bool = false) -> String {
|
||||||
|
var displayedText = compositor.keys.joined(separator: "\t")
|
||||||
|
if compositor.isEmpty {
|
||||||
|
displayedText = readingForDisplay
|
||||||
|
}
|
||||||
|
if !prefs.cassetteEnabled {
|
||||||
|
if prefs.inlineDumpPinyinInLieuOfZhuyin {
|
||||||
|
if !compositor.isEmpty {
|
||||||
|
var arrDisplayedTextElements = [String]()
|
||||||
|
compositor.keys.forEach { key in
|
||||||
|
arrDisplayedTextElements.append(Tekkon.restoreToneOneInPhona(target: key)) // 恢復陰平標記
|
||||||
|
}
|
||||||
|
displayedText = arrDisplayedTextElements.joined(separator: "\t")
|
||||||
|
}
|
||||||
|
displayedText = Tekkon.cnvPhonaToHanyuPinyin(targetJoined: displayedText) // 注音轉拼音
|
||||||
|
}
|
||||||
|
if prefs.showHanyuPinyinInCompositionBuffer {
|
||||||
|
if compositor.isEmpty {
|
||||||
|
displayedText = displayedText.replacingOccurrences(of: "1", with: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
displayedText = displayedText.replacingOccurrences(of: "\t", with: isShiftPressed ? "-" : " ")
|
||||||
|
return displayedText
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Command+Option+Enter 鍵的處理(網頁 Ruby 注音文標記)
|
||||||
|
|
||||||
|
/// Command+Option+Enter 鍵的處理(網頁 Ruby 注音文標記)。
|
||||||
|
/// - Parameter isShiftPressed: 有沒有同時摁著 Shift 鍵。摁了的話則只遞交讀音字串。
|
||||||
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 SessionCtl 回報給 IMK。
|
||||||
|
private func commissionByCtrlOptionCommandEnter(isShiftPressed: Bool = false) -> String {
|
||||||
|
var composed = ""
|
||||||
|
|
||||||
|
compositor.walkedNodes.smashedPairs.forEach { key, value in
|
||||||
|
var key = key
|
||||||
|
if !prefs.cassetteEnabled {
|
||||||
|
key =
|
||||||
|
prefs.inlineDumpPinyinInLieuOfZhuyin
|
||||||
|
? Tekkon.restoreToneOneInPhona(target: key) // 恢復陰平標記
|
||||||
|
: Tekkon.cnvPhonaToTextbookReading(target: key) // 恢復陰平標記
|
||||||
|
|
||||||
|
if prefs.inlineDumpPinyinInLieuOfZhuyin {
|
||||||
|
key = Tekkon.cnvPhonaToHanyuPinyin(targetJoined: key) // 注音轉拼音
|
||||||
|
key = Tekkon.cnvHanyuPinyinToTextbookStyle(targetJoined: key) // 轉教科書式標調
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
key = key.replacingOccurrences(of: "\t", with: " ")
|
||||||
|
|
||||||
|
if isShiftPressed {
|
||||||
|
if !composed.isEmpty { composed += " " }
|
||||||
|
composed += key.contains("_") ? "??" : key
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不要給標點符號等特殊元素加注音
|
||||||
|
composed += key.contains("_") ? value : "<ruby>\(value)<rp>(</rp><rt>\(key)</rt><rp>)</rp></ruby>"
|
||||||
|
}
|
||||||
|
|
||||||
|
return composed
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - 處理 BackSpace (macOS Delete) 按鍵行為
|
// MARK: - 處理 BackSpace (macOS Delete) 按鍵行為
|
||||||
|
|
||||||
/// 處理 BackSpace (macOS Delete) 按鍵行為。
|
/// 處理 BackSpace (macOS Delete) 按鍵行為。
|
||||||
|
@ -403,28 +465,31 @@ extension InputHandler {
|
||||||
guard let delegate = delegate else { return false }
|
guard let delegate = delegate else { return false }
|
||||||
let state = delegate.state
|
let state = delegate.state
|
||||||
guard state.type == .ofInputting else {
|
guard state.type == .ofInputting else {
|
||||||
currentTypingMethod = .vChewingFactory
|
isCodePointInputMode = false
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if currentTypingMethod == .codePoint {
|
if isCodePointInputMode {
|
||||||
if !strCodePointBuffer.isEmpty {
|
if !strCodePointBuffer.isEmpty {
|
||||||
func refreshState() {
|
func refreshState() {
|
||||||
var updatedState = generateStateOfInputting(guarded: true)
|
var updatedState = generateStateOfInputting(guarded: true)
|
||||||
updatedState.tooltipDuration = 0
|
updatedState.tooltipDuration = 0
|
||||||
updatedState.tooltip = delegate.state.tooltip
|
updatedState.tooltip = tooltipCodePointInputMode
|
||||||
delegate.switchState(updatedState)
|
delegate.switchState(updatedState)
|
||||||
}
|
}
|
||||||
strCodePointBuffer = strCodePointBuffer.dropLast(1).description
|
strCodePointBuffer = strCodePointBuffer.dropLast(1).description
|
||||||
if input.commonKeyModifierFlags == .option {
|
if input.commonKeyModifierFlags == .option {
|
||||||
return revolveTypingMethod(to: .codePoint)
|
strCodePointBuffer.removeAll()
|
||||||
|
refreshState()
|
||||||
|
isCodePointInputMode = true
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
if !strCodePointBuffer.isEmpty {
|
if !strCodePointBuffer.isEmpty {
|
||||||
refreshState()
|
refreshState()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return revolveTypingMethod(to: .vChewingFactory)
|
return handleCodePointInputToggle()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 引入 macOS 內建注音輸入法的行為,允許用 Shift+BackSpace 解構前一個漢字的讀音。
|
// 引入 macOS 內建注音輸入法的行為,允許用 Shift+BackSpace 解構前一個漢字的讀音。
|
||||||
|
@ -505,9 +570,8 @@ extension InputHandler {
|
||||||
guard let delegate = delegate else { return false }
|
guard let delegate = delegate else { return false }
|
||||||
let state = delegate.state
|
let state = delegate.state
|
||||||
|
|
||||||
guard currentTypingMethod == .vChewingFactory else {
|
if isHaninKeyboardSymbolMode { return handleHaninKeyboardSymbolModeToggle() }
|
||||||
return revolveTypingMethod(to: .vChewingFactory)
|
if isCodePointInputMode { return handleCodePointInputToggle() }
|
||||||
}
|
|
||||||
|
|
||||||
guard state.type == .ofInputting else { return false }
|
guard state.type == .ofInputting else { return false }
|
||||||
|
|
||||||
|
@ -615,9 +679,8 @@ extension InputHandler {
|
||||||
guard let delegate = delegate else { return false }
|
guard let delegate = delegate else { return false }
|
||||||
let state = delegate.state
|
let state = delegate.state
|
||||||
|
|
||||||
guard currentTypingMethod == .vChewingFactory else {
|
if isHaninKeyboardSymbolMode { return handleHaninKeyboardSymbolModeToggle() }
|
||||||
return revolveTypingMethod(to: .vChewingFactory)
|
if isCodePointInputMode { return handleCodePointInputToggle() }
|
||||||
}
|
|
||||||
|
|
||||||
guard state.type == .ofInputting else { return false }
|
guard state.type == .ofInputting else { return false }
|
||||||
|
|
||||||
|
@ -852,6 +915,73 @@ extension InputHandler {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - 處理內碼區位輸入狀態的啟動過程(CodePoint Input Toggle)
|
||||||
|
|
||||||
|
@discardableResult func handleCodePointInputToggle() -> Bool {
|
||||||
|
guard let delegate = delegate, delegate.state.type != .ofDeactivated else { return false }
|
||||||
|
if isCodePointInputMode {
|
||||||
|
isCodePointInputMode = false
|
||||||
|
delegate.switchState(IMEState.ofAbortion())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
var updatedState = generateStateOfInputting(sansReading: true)
|
||||||
|
delegate.switchState(IMEState.ofCommitting(textToCommit: updatedState.displayedText))
|
||||||
|
updatedState = generateStateOfInputting(guarded: true)
|
||||||
|
updatedState.tooltipDuration = 0
|
||||||
|
updatedState.tooltip = tooltipCodePointInputMode
|
||||||
|
delegate.switchState(updatedState)
|
||||||
|
isCodePointInputMode = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 處理漢音鍵盤符號輸入狀態的啟動過程(Hanin Pallete)
|
||||||
|
|
||||||
|
@discardableResult func handleHaninKeyboardSymbolModeToggle() -> Bool {
|
||||||
|
guard let delegate = delegate, delegate.state.type != .ofDeactivated else { return false }
|
||||||
|
if isCodePointInputMode { isCodePointInputMode = false }
|
||||||
|
if isHaninKeyboardSymbolMode {
|
||||||
|
isHaninKeyboardSymbolMode = false
|
||||||
|
delegate.switchState(IMEState.ofAbortion())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
var updatedState = generateStateOfInputting(sansReading: true)
|
||||||
|
delegate.switchState(IMEState.ofCommitting(textToCommit: updatedState.displayedText))
|
||||||
|
updatedState = generateStateOfInputting(guarded: true)
|
||||||
|
updatedState.tooltipDuration = 0
|
||||||
|
updatedState.tooltip = Self.tooltipHaninKeyboardSymbolMode
|
||||||
|
delegate.switchState(updatedState)
|
||||||
|
isHaninKeyboardSymbolMode = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 處理漢音鍵盤符號輸入。
|
||||||
|
/// - Parameters:
|
||||||
|
/// - input: 輸入按鍵訊號。
|
||||||
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 SessionCtl 回報給 IMK。
|
||||||
|
func handleHaninKeyboardSymbolModeInput(input: InputSignalProtocol) -> Bool {
|
||||||
|
guard let delegate = delegate, delegate.state.type != .ofDeactivated else { return false }
|
||||||
|
let charText = input.text.lowercased().applyingTransformFW2HW(reverse: false)
|
||||||
|
guard CandidateNode.mapHaninKeyboardSymbols.keys.contains(charText) else {
|
||||||
|
return handleHaninKeyboardSymbolModeToggle()
|
||||||
|
}
|
||||||
|
guard
|
||||||
|
charText.count == 1, let symbols = CandidateNode.queryHaninKeyboardSymbols(char: charText)
|
||||||
|
else {
|
||||||
|
delegate.callError("C1A760C7")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// 得在這裡先 commit buffer,不然會導致「在摁 ESC 離開符號選單時會重複輸入上一次的組字區的內容」的不當行為。
|
||||||
|
let textToCommit = generateStateOfInputting(sansReading: true).displayedText
|
||||||
|
delegate.switchState(IMEState.ofCommitting(textToCommit: textToCommit))
|
||||||
|
if symbols.members.count == 1 {
|
||||||
|
delegate.switchState(IMEState.ofCommitting(textToCommit: symbols.members.map(\.name).joined()))
|
||||||
|
} else {
|
||||||
|
delegate.switchState(IMEState.ofSymbolTable(node: symbols))
|
||||||
|
}
|
||||||
|
isHaninKeyboardSymbolMode = false // 用完就關掉,但保持選字窗開啟,所以這裡不用呼叫 toggle 函式。
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - 處理符號選單(Symbol Menu Input)
|
// MARK: - 處理符號選單(Symbol Menu Input)
|
||||||
|
|
||||||
/// 處理符號選單。
|
/// 處理符號選單。
|
||||||
|
@ -871,7 +1001,7 @@ extension InputHandler {
|
||||||
inputting.textToCommit = textToCommit
|
inputting.textToCommit = textToCommit
|
||||||
delegate.switchState(inputting)
|
delegate.switchState(inputting)
|
||||||
// 開始決定是否切換至選字狀態。
|
// 開始決定是否切換至選字狀態。
|
||||||
let newState = generateStateOfCandidates(dodge: false)
|
let newState = generateStateOfCandidates()
|
||||||
_ = newState.candidates.isEmpty ? delegate.callError("B5127D8A") : delegate.switchState(newState)
|
_ = newState.candidates.isEmpty ? delegate.callError("B5127D8A") : delegate.switchState(newState)
|
||||||
} else { // 不要在注音沒敲完整的情況下叫出統合符號選單。
|
} else { // 不要在注音沒敲完整的情況下叫出統合符號選單。
|
||||||
delegate.callError("17446655")
|
delegate.callError("17446655")
|
||||||
|
@ -898,20 +1028,6 @@ extension InputHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 處理選字窗服務選單 (Service Menu)
|
|
||||||
|
|
||||||
func handleServiceMenuInitiation(candidateText: String, reading: [String]) -> Bool {
|
|
||||||
guard let delegate = delegate, delegate.state.type != .ofDeactivated else { return false }
|
|
||||||
guard !candidateText.isEmpty else { return false }
|
|
||||||
let rootNode = CandidateTextService.getCurrentServiceMenu(candidate: candidateText, reading: reading)
|
|
||||||
guard let rootNode = rootNode else { return false }
|
|
||||||
// 得在這裡先 commit buffer,不然會導致「在摁 ESC 離開符號選單時會重複輸入上一次的組字區的內容」的不當行為。
|
|
||||||
let textToCommit = generateStateOfInputting(sansReading: true).displayedText
|
|
||||||
delegate.switchState(IMEState.ofCommitting(textToCommit: textToCommit))
|
|
||||||
delegate.switchState(IMEState.ofSymbolTable(node: rootNode))
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 處理 Caps Lock 與英數輸入模式(Caps Lock and Alphanumerical mode)
|
// MARK: - 處理 Caps Lock 與英數輸入模式(Caps Lock and Alphanumerical mode)
|
||||||
|
|
||||||
/// 處理 CapsLock 與英數輸入模式。
|
/// 處理 CapsLock 與英數輸入模式。
|
||||||
|
@ -1041,15 +1157,11 @@ extension InputHandler {
|
||||||
let fullWidthResult = behaviorValue % 2 != 0 // 能被二整除的都是半形。
|
let fullWidthResult = behaviorValue % 2 != 0 // 能被二整除的都是半形。
|
||||||
triagePrefs: switch (behaviorValue, isConsideredEmptyForNow) {
|
triagePrefs: switch (behaviorValue, isConsideredEmptyForNow) {
|
||||||
case (2, _), (3, _), (4, false), (5, false):
|
case (2, _), (3, _), (4, false), (5, false):
|
||||||
currentLM.setOptions { config in
|
currentLM.config.numPadFWHWStatus = fullWidthResult
|
||||||
config.numPadFWHWStatus = fullWidthResult
|
|
||||||
}
|
|
||||||
if handlePunctuation("_NumPad_\(inputText)") { return true }
|
if handlePunctuation("_NumPad_\(inputText)") { return true }
|
||||||
default: break triagePrefs // 包括 case 0 & 1。
|
default: break triagePrefs // 包括 case 0 & 1。
|
||||||
}
|
}
|
||||||
currentLM.setOptions { config in
|
currentLM.config.numPadFWHWStatus = nil
|
||||||
config.numPadFWHWStatus = nil
|
|
||||||
}
|
|
||||||
delegate.switchState(IMEState.ofEmpty())
|
delegate.switchState(IMEState.ofEmpty())
|
||||||
let charToCommit = inputText.applyingTransformFW2HW(reverse: fullWidthResult)
|
let charToCommit = inputText.applyingTransformFW2HW(reverse: fullWidthResult)
|
||||||
delegate.switchState(IMEState.ofCommitting(textToCommit: charToCommit))
|
delegate.switchState(IMEState.ofCommitting(textToCommit: charToCommit))
|
|
@ -9,11 +9,11 @@
|
||||||
/// 該檔案乃輸入調度模組當中「用來規定當 IMK 接受按鍵訊號時且首次交給輸入調度模組處理時、
|
/// 該檔案乃輸入調度模組當中「用來規定當 IMK 接受按鍵訊號時且首次交給輸入調度模組處理時、
|
||||||
/// 輸入調度模組要率先處理」的部分。據此判斷是否需要將按鍵處理委派給其它成員函式。
|
/// 輸入調度模組要率先處理」的部分。據此判斷是否需要將按鍵處理委派給其它成員函式。
|
||||||
|
|
||||||
|
import CocoaExtension
|
||||||
import IMKUtils
|
import IMKUtils
|
||||||
import InputMethodKit
|
import InputMethodKit
|
||||||
import LangModelAssembly
|
import LangModelAssembly
|
||||||
import Megrez
|
import Megrez
|
||||||
import OSFrameworkImpl
|
|
||||||
import Shared
|
import Shared
|
||||||
|
|
||||||
// MARK: - § 根據狀態調度按鍵輸入 (Handle Input with States) * Triage
|
// MARK: - § 根據狀態調度按鍵輸入 (Handle Input with States) * Triage
|
||||||
|
@ -48,7 +48,7 @@ public extension InputHandler {
|
||||||
case .kCarriageReturn, .kLineFeed:
|
case .kCarriageReturn, .kLineFeed:
|
||||||
let frontNode = compositor.walkedNodes.last
|
let frontNode = compositor.walkedNodes.last
|
||||||
return handleEnter(input: input) {
|
return handleEnter(input: input) {
|
||||||
guard self.currentTypingMethod == .vChewingFactory else { return [] }
|
guard !self.isHaninKeyboardSymbolMode, !self.isCodePointInputMode else { return [] }
|
||||||
guard let frontNode = frontNode else { return [] }
|
guard let frontNode = frontNode else { return [] }
|
||||||
let pair = Megrez.KeyValuePaired(keyArray: frontNode.keyArray, value: frontNode.value)
|
let pair = Megrez.KeyValuePaired(keyArray: frontNode.keyArray, value: frontNode.value)
|
||||||
let associates = self.generateArrayOfAssociates(withPair: pair)
|
let associates = self.generateArrayOfAssociates(withPair: pair)
|
||||||
|
@ -62,7 +62,13 @@ public extension InputHandler {
|
||||||
case [.option, .shift]:
|
case [.option, .shift]:
|
||||||
return handlePunctuationList(alternative: true, isJIS: isJIS)
|
return handlePunctuationList(alternative: true, isJIS: isJIS)
|
||||||
case .option:
|
case .option:
|
||||||
return revolveTypingMethod()
|
switch (isCodePointInputMode, isHaninKeyboardSymbolMode) {
|
||||||
|
case (false, false): return handleCodePointInputToggle()
|
||||||
|
case (true, false), (false, true):
|
||||||
|
return handleHaninKeyboardSymbolModeToggle()
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
return true
|
||||||
default: break
|
default: break
|
||||||
}
|
}
|
||||||
case .kSpace:
|
case .kSpace:
|
||||||
|
@ -79,7 +85,7 @@ public extension InputHandler {
|
||||||
if input.isShiftHold, !input.isControlHold, !input.isOptionHold {
|
if input.isShiftHold, !input.isControlHold, !input.isOptionHold {
|
||||||
return revolveCandidate(reverseOrder: input.isCommandHold)
|
return revolveCandidate(reverseOrder: input.isCommandHold)
|
||||||
}
|
}
|
||||||
if currentTypingMethod == .codePoint {
|
if isCodePointInputMode {
|
||||||
delegate.callError("FDD88EDB")
|
delegate.callError("FDD88EDB")
|
||||||
delegate.switchState(IMEState.ofAbortion())
|
delegate.switchState(IMEState.ofAbortion())
|
||||||
return true
|
return true
|
||||||
|
@ -142,11 +148,13 @@ public extension InputHandler {
|
||||||
guard let x = input.inputTextIgnoringModifiers,
|
guard let x = input.inputTextIgnoringModifiers,
|
||||||
"¥\\".contains(x), input.keyModifierFlags.isEmpty
|
"¥\\".contains(x), input.keyModifierFlags.isEmpty
|
||||||
else { break haninSymbolInput }
|
else { break haninSymbolInput }
|
||||||
return revolveTypingMethod(to: .haninKeyboardSymbol)
|
return handleHaninKeyboardSymbolModeToggle()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注音/磁帶按鍵輸入與漢音鍵盤符號輸入處理。
|
// 注音按鍵輸入與漢音鍵盤符號輸入處理。
|
||||||
if let compositionHandled = handleComposition(input: input) {
|
if isHaninKeyboardSymbolMode, [[], .shift].contains(input.keyModifierFlags) {
|
||||||
|
return handleHaninKeyboardSymbolModeInput(input: input)
|
||||||
|
} else if let compositionHandled = handleComposition(input: input) {
|
||||||
return compositionHandled
|
return compositionHandled
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,20 +15,26 @@ import SwiftExtension
|
||||||
// MARK: - Input Mode Extension for Language Models
|
// MARK: - Input Mode Extension for Language Models
|
||||||
|
|
||||||
public extension Shared.InputMode {
|
public extension Shared.InputMode {
|
||||||
private static let lmCHS = LMAssembly.LMInstantiator(
|
private static let lmCHS = vChewingLM.LMInstantiator(isCHS: true)
|
||||||
isCHS: true, uomDataURL: LMMgr.userOverrideModelDataURL(.imeModeCHS)
|
private static let lmCHT = vChewingLM.LMInstantiator(isCHS: false)
|
||||||
)
|
private static let uomCHS = vChewingLM.LMUserOverride(dataURL: LMMgr.userOverrideModelDataURL(.imeModeCHS))
|
||||||
private static let lmCHT = LMAssembly.LMInstantiator(
|
private static let uomCHT = vChewingLM.LMUserOverride(dataURL: LMMgr.userOverrideModelDataURL(.imeModeCHT))
|
||||||
isCHS: false, uomDataURL: LMMgr.userOverrideModelDataURL(.imeModeCHT)
|
|
||||||
)
|
|
||||||
|
|
||||||
var langModel: LMAssembly.LMInstantiator {
|
var langModel: vChewingLM.LMInstantiator {
|
||||||
switch self {
|
switch self {
|
||||||
case .imeModeCHS: return Self.lmCHS
|
case .imeModeCHS: return Self.lmCHS
|
||||||
case .imeModeCHT: return Self.lmCHT
|
case .imeModeCHT: return Self.lmCHT
|
||||||
case .imeModeNULL: return .init()
|
case .imeModeNULL: return .init()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var uom: vChewingLM.LMUserOverride {
|
||||||
|
switch self {
|
||||||
|
case .imeModeCHS: return Self.uomCHS
|
||||||
|
case .imeModeCHT: return Self.uomCHT
|
||||||
|
case .imeModeNULL: return .init(dataURL: LMMgr.userOverrideModelDataURL(IMEApp.currentInputMode))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Language Model Manager.
|
// MARK: - Language Model Manager.
|
||||||
|
@ -48,14 +54,14 @@ public class LMMgr {
|
||||||
Self.loadUserPhrasesData()
|
Self.loadUserPhrasesData()
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var isCoreDBConnected: Bool { LMAssembly.LMInstantiator.isSQLDBConnected }
|
public static var isCoreDBConnected: Bool { vChewingLM.LMInstantiator.isSQLDBConnected }
|
||||||
|
|
||||||
public static func connectCoreDB(dbPath: String? = nil) {
|
public static func connectCoreDB(dbPath: String? = nil) {
|
||||||
guard let path: String = dbPath ?? Self.getCoreDictionaryDBPath() else {
|
guard let path: String = dbPath ?? Self.getCoreDictionaryDBPath() else {
|
||||||
assertionFailure("vChewing factory SQLite data not found.")
|
assertionFailure("vChewing factory SQLite data not found.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let result = LMAssembly.LMInstantiator.connectSQLDB(dbPath: path)
|
let result = vChewingLM.LMInstantiator.connectSQLDB(dbPath: path)
|
||||||
assert(result, "vChewing factory SQLite connection failed.")
|
assert(result, "vChewing factory SQLite connection failed.")
|
||||||
Notifier.notify(
|
Notifier.notify(
|
||||||
message: NSLocalizedString("Core Dict loading complete.", comment: "")
|
message: NSLocalizedString("Core Dict loading complete.", comment: "")
|
||||||
|
@ -65,15 +71,10 @@ public class LMMgr {
|
||||||
/// 載入磁帶資料。
|
/// 載入磁帶資料。
|
||||||
/// - Remark: cassettePath() 會在輸入法停用磁帶時直接返回
|
/// - Remark: cassettePath() 會在輸入法停用磁帶時直接返回
|
||||||
public static func loadCassetteData() {
|
public static func loadCassetteData() {
|
||||||
func validateCassetteCandidateKey(_ target: String) -> Bool {
|
vChewingLM.LMInstantiator.loadCassetteData(path: cassettePath())
|
||||||
CandidateKey.validate(keys: target) == nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LMAssembly.LMInstantiator.setCassetCandidateKeyValidator(validateCassetteCandidateKey)
|
public static func loadUserPhrasesData(type: vChewingLM.ReplacableUserDataType? = nil) {
|
||||||
LMAssembly.LMInstantiator.loadCassetteData(path: cassettePath())
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func loadUserPhrasesData(type: LMAssembly.ReplacableUserDataType? = nil) {
|
|
||||||
guard let type = type else {
|
guard let type = type else {
|
||||||
Shared.InputMode.validCases.forEach { mode in
|
Shared.InputMode.validCases.forEach { mode in
|
||||||
mode.langModel.loadUserPhrasesData(
|
mode.langModel.loadUserPhrasesData(
|
||||||
|
@ -81,11 +82,12 @@ public class LMMgr {
|
||||||
filterPath: userDictDataURL(mode: mode, type: .theFilter).path
|
filterPath: userDictDataURL(mode: mode, type: .theFilter).path
|
||||||
)
|
)
|
||||||
mode.langModel.loadUserSymbolData(path: userDictDataURL(mode: mode, type: .theSymbols).path)
|
mode.langModel.loadUserSymbolData(path: userDictDataURL(mode: mode, type: .theSymbols).path)
|
||||||
mode.langModel.loadUOMData()
|
mode.uom.loadData(fromURL: userOverrideModelDataURL(mode))
|
||||||
}
|
}
|
||||||
|
|
||||||
if PrefMgr.shared.associatedPhrasesEnabled { Self.loadUserAssociatesData() }
|
if PrefMgr.shared.associatedPhrasesEnabled { Self.loadUserAssociatesData() }
|
||||||
if PrefMgr.shared.phraseReplacementEnabled { Self.loadUserPhraseReplacement() }
|
if PrefMgr.shared.phraseReplacementEnabled { Self.loadUserPhraseReplacement() }
|
||||||
|
if PrefMgr.shared.useSCPCTypingMode { Self.loadSCPCSequencesData() }
|
||||||
|
|
||||||
CandidateNode.load(url: Self.userSymbolMenuDataURL())
|
CandidateNode.load(url: Self.userSymbolMenuDataURL())
|
||||||
return
|
return
|
||||||
|
@ -129,6 +131,12 @@ public class LMMgr {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func loadSCPCSequencesData() {
|
||||||
|
Shared.InputMode.validCases.forEach { mode in
|
||||||
|
mode.langModel.loadSCPCSequencesData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static func reloadUserFilterDirectly(mode: Shared.InputMode) {
|
public static func reloadUserFilterDirectly(mode: Shared.InputMode) {
|
||||||
mode.langModel.reloadUserFilterDirectly(path: userDictDataURL(mode: mode, type: .theFilter).path)
|
mode.langModel.reloadUserFilterDirectly(path: userDictDataURL(mode: mode, type: .theFilter).path)
|
||||||
}
|
}
|
||||||
|
@ -170,7 +178,6 @@ public class LMMgr {
|
||||||
config.isSymbolEnabled = PrefMgr.shared.symbolInputEnabled
|
config.isSymbolEnabled = PrefMgr.shared.symbolInputEnabled
|
||||||
config.isSCPCEnabled = PrefMgr.shared.useSCPCTypingMode
|
config.isSCPCEnabled = PrefMgr.shared.useSCPCTypingMode
|
||||||
config.isCassetteEnabled = PrefMgr.shared.cassetteEnabled
|
config.isCassetteEnabled = PrefMgr.shared.cassetteEnabled
|
||||||
config.filterNonCNSReadings = PrefMgr.shared.filterNonCNSReadingsForCHTInput
|
|
||||||
config.deltaOfCalendarYears = PrefMgr.shared.deltaOfCalendarYears
|
config.deltaOfCalendarYears = PrefMgr.shared.deltaOfCalendarYears
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -179,12 +186,12 @@ public class LMMgr {
|
||||||
// MARK: UOM
|
// MARK: UOM
|
||||||
|
|
||||||
public static func saveUserOverrideModelData() {
|
public static func saveUserOverrideModelData() {
|
||||||
let globalQueue = DispatchQueue(label: "LMAssembly_UOM", qos: .unspecified, attributes: .concurrent)
|
let globalQueue = DispatchQueue(label: "vChewingLM_UOM", qos: .unspecified, attributes: .concurrent)
|
||||||
let group = DispatchGroup()
|
let group = DispatchGroup()
|
||||||
Shared.InputMode.validCases.forEach { mode in
|
Shared.InputMode.validCases.forEach { mode in
|
||||||
group.enter()
|
group.enter()
|
||||||
globalQueue.async {
|
globalQueue.async {
|
||||||
mode.langModel.saveUOMData()
|
mode.uom.saveData(toURL: userOverrideModelDataURL(mode))
|
||||||
group.leave()
|
group.leave()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -193,11 +200,11 @@ public class LMMgr {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func bleachSpecifiedSuggestions(targets: [String], mode: Shared.InputMode) {
|
public static func bleachSpecifiedSuggestions(targets: [String], mode: Shared.InputMode) {
|
||||||
mode.langModel.bleachSpecifiedUOMSuggestions(targets: targets)
|
mode.uom.bleachSpecifiedSuggestions(targets: targets, saveCallback: { mode.uom.saveData() })
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func removeUnigramsFromUserOverrideModel(_ mode: Shared.InputMode) {
|
public static func removeUnigramsFromUserOverrideModel(_ mode: Shared.InputMode) {
|
||||||
mode.langModel.bleachUOMUnigrams()
|
mode.uom.bleachUnigrams(saveCallback: { mode.uom.saveData() })
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func relocateWreckedUOMData() {
|
public static func relocateWreckedUOMData() {
|
||||||
|
@ -219,6 +226,6 @@ public class LMMgr {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func clearUserOverrideModelData(_ mode: Shared.InputMode = .imeModeNULL) {
|
public static func clearUserOverrideModelData(_ mode: Shared.InputMode = .imeModeNULL) {
|
||||||
mode.langModel.clearUOMData()
|
mode.uom.clearData(withURL: userOverrideModelDataURL(mode))
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
// (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 LineReader
|
||||||
|
|
||||||
|
public extension LMMgr {
|
||||||
|
/// 匯入自奇摩輸入法使用者自訂詞資料庫匯出的 TXT 檔案。
|
||||||
|
/// - Parameter rawString: 原始 TXT 檔案內容。
|
||||||
|
/// - Returns: 成功匯入的資料數量。
|
||||||
|
@discardableResult static func importYahooKeyKeyUserDictionary(text rawString: inout String) -> Int {
|
||||||
|
var allPhrasesCHT = [UserPhrase]()
|
||||||
|
rawString.enumerateLines { currentLine, _ in
|
||||||
|
let cells = currentLine.split(separator: "\t")
|
||||||
|
guard cells.count >= 3, cells.first != "#", cells.first != "MJSR" else { return }
|
||||||
|
let value = cells[0].description
|
||||||
|
let keyArray = cells[1].split(separator: ",").map(\.description)
|
||||||
|
let phraseCHT = UserPhrase(keyArray: keyArray, value: value, inputMode: .imeModeCHT, isConverted: false)
|
||||||
|
guard phraseCHT.isValid, !phraseCHT.isDuplicated else { return }
|
||||||
|
guard !(phraseCHT.value.count == 1 && phraseCHT.keyArray.count == 1) else { return }
|
||||||
|
allPhrasesCHT.append(phraseCHT)
|
||||||
|
}
|
||||||
|
guard !allPhrasesCHT.isEmpty else { return 0 }
|
||||||
|
let allPhrasesCHS = allPhrasesCHT.compactMap { chtPhrase in
|
||||||
|
let chsPhrase = chtPhrase.crossConverted
|
||||||
|
return chsPhrase.isValid && !chsPhrase.isDuplicated ? chsPhrase : nil
|
||||||
|
}.deduplicated
|
||||||
|
let outputStrCHS = allPhrasesCHS.map(\.description).joined(separator: "\n")
|
||||||
|
let outputStrCHT = allPhrasesCHT.map(\.description).joined(separator: "\n")
|
||||||
|
var outputDataCHS = "\(outputStrCHS)\n".data(using: .utf8) ?? .init([])
|
||||||
|
var outputDataCHT = "\(outputStrCHT)\n".data(using: .utf8) ?? .init([])
|
||||||
|
let urlCHS = LMMgr.userDictDataURL(mode: .imeModeCHS, type: .thePhrases)
|
||||||
|
let urlCHT = LMMgr.userDictDataURL(mode: .imeModeCHT, type: .thePhrases)
|
||||||
|
|
||||||
|
let fileHandlerCHS = try? FileHandle(forUpdating: urlCHS)
|
||||||
|
let fileHandlerCHT = try? FileHandle(forUpdating: urlCHT)
|
||||||
|
guard let fileHandlerCHS = fileHandlerCHS, let fileHandlerCHT = fileHandlerCHT else { return 0 }
|
||||||
|
defer {
|
||||||
|
fileHandlerCHS.closeFile()
|
||||||
|
fileHandlerCHT.closeFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
if let sizeCHS = fileSize(for: urlCHS), sizeCHS > 0 {
|
||||||
|
fileHandlerCHS.seek(toFileOffset: sizeCHS)
|
||||||
|
if fileHandlerCHS.readDataToEndOfFile().first != 0x0A {
|
||||||
|
outputDataCHS.insert(0x0A, at: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileHandlerCHS.seekToEndOfFile()
|
||||||
|
fileHandlerCHS.write(outputDataCHS)
|
||||||
|
|
||||||
|
if let sizeCHT = fileSize(for: urlCHT), sizeCHT > 0 {
|
||||||
|
fileHandlerCHT.seek(toFileOffset: sizeCHT)
|
||||||
|
if fileHandlerCHT.readDataToEndOfFile().first != 0x0A {
|
||||||
|
outputDataCHT.insert(0x0A, at: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileHandlerCHT.seekToEndOfFile()
|
||||||
|
fileHandlerCHT.write(outputDataCHT)
|
||||||
|
|
||||||
|
return allPhrasesCHT.count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fileSize(for theURL: URL) -> UInt64? {
|
||||||
|
(try? FileManager.default.attributesOfItem(atPath: theURL.path))?[FileAttributeKey.size] as? UInt64
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue