Compare commits
No commits in common. "main" and "3.6.1" have entirely different histories.
|
@ -18,14 +18,14 @@
|
|||
[branch "upd/dev"]
|
||||
remote = origin
|
||||
merge = refs/heads/upd/dev
|
||||
[remote "gitlink"]
|
||||
url = https://gitlink.org.cn/vChewing/vChewing-macOS.git
|
||||
fetch = +refs/heads/*:refs/remotes/gitlink/*
|
||||
[branch "bleed/1.5.x"]
|
||||
remote = origin
|
||||
merge = refs/heads/bleed/1.5.x
|
||||
[remote "gitcode"]
|
||||
url = https://gitcode.net/vChewing/vChewing-macOS.git/
|
||||
fetch = +refs/heads/*:refs/remotes/gitcode/*
|
||||
[remote "gitlab"]
|
||||
url = https://gitlab.com/vChewing/vChewing-macOS.git/
|
||||
url = https://jihulab.com/vChewing/vChewing-macOS.git/
|
||||
fetch = +refs/heads/*:refs/remotes/gitlab/*
|
||||
[remote "github"]
|
||||
url = https://github.com/vChewing/vChewing-macOS/
|
||||
|
@ -34,7 +34,6 @@
|
|||
url = https://gitee.com/vChewing/vChewing-macOS.git/
|
||||
fetch = +refs/heads/*:refs/remotes/all/*
|
||||
pushurl = https://gitee.com/vchewing/vChewing-macOS.git/
|
||||
pushurl = https://gitlink.org.cn/vChewing/vChewing-macOS.git/
|
||||
pushurl = https://gitcode.net/vChewing/vChewing-macOS.git/
|
||||
pushurl = https://gitlab.com/vChewing/vChewing-macOS.git/
|
||||
pushurl = https://jihulab.com/vChewing/vChewing-macOS.git/
|
||||
pushurl = https://github.com/vChewing/vChewing-macOS/
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
name: debug-macOS-MainAssembly
|
||||
name: Build-with-macOS-latest
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
|
@ -7,16 +7,16 @@ on:
|
|||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: macOS-13
|
||||
name: Build (latest)
|
||||
runs-on: macOS-latest
|
||||
env:
|
||||
GIT_SSL_NO_VERIFY: true
|
||||
steps:
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: '^15.1'
|
||||
xcode-version: '^15.0'
|
||||
- uses: actions/checkout@v1
|
||||
- name: Clean
|
||||
run: make spmClean
|
||||
run: make clean
|
||||
- name: Build
|
||||
run: make spmDebug
|
||||
run: git pull --all && git submodule sync; make update; make
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import Foundation
|
||||
import SQLite3
|
||||
|
||||
// MARK: - 前導工作
|
||||
|
||||
|
@ -28,14 +27,6 @@ fileprivate extension String {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - String as SQL Command
|
||||
|
||||
fileprivate extension String {
|
||||
@discardableResult func runAsSQLExec(dbPointer ptrDB: inout OpaquePointer?) -> Bool {
|
||||
ptrDB != nil && sqlite3_exec(ptrDB, self, nil, nil, nil) == SQLITE_OK
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - StringView Ranges Extension (by Isaac Xen)
|
||||
|
||||
fileprivate extension String {
|
||||
|
@ -126,125 +117,40 @@ func cnvPhonabetToASCII(_ incoming: String) -> String {
|
|||
|
||||
private let urlCurrentFolder = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
|
||||
|
||||
private let urlCHSRoot: String = "\(urlCurrentFolder.path)/components/chs/"
|
||||
private let urlCHTRoot: String = "\(urlCurrentFolder.path)/components/cht/"
|
||||
private let urlCHSRoot: String = "./components/chs/"
|
||||
private let urlCHTRoot: String = "./components/cht/"
|
||||
|
||||
private let urlKanjiCore: String = "\(urlCurrentFolder.path)/components/common/char-kanji-core.txt"
|
||||
private let urlMiscBPMF: String = "\(urlCurrentFolder.path)/components/common/char-misc-bpmf.txt"
|
||||
private let urlMiscNonKanji: String = "\(urlCurrentFolder.path)/components/common/char-misc-nonkanji.txt"
|
||||
private let urlKanjiCore: String = "./components/common/char-kanji-core.txt"
|
||||
private let urlMiscBPMF: String = "./components/common/char-misc-bpmf.txt"
|
||||
private let urlMiscNonKanji: String = "./components/common/char-misc-nonkanji.txt"
|
||||
|
||||
private let urlPunctuation: String = "\(urlCurrentFolder.path)/components/common/data-punctuations.txt"
|
||||
private let urlSymbols: String = "\(urlCurrentFolder.path)/components/common/data-symbols.txt"
|
||||
private let urlZhuyinwen: String = "\(urlCurrentFolder.path)/components/common/data-zhuyinwen.txt"
|
||||
private let urlCNS: String = "\(urlCurrentFolder.path)/components/common/char-kanji-cns.txt"
|
||||
private let urlPunctuation: String = "./components/common/data-punctuations.txt"
|
||||
private let urlSymbols: String = "./components/common/data-symbols.txt"
|
||||
private let urlZhuyinwen: String = "./components/common/data-zhuyinwen.txt"
|
||||
private let urlCNS: String = "./components/common/char-kanji-cns.txt"
|
||||
|
||||
private let urlOutputCHS: String = "\(urlCurrentFolder.path)/data-chs.txt"
|
||||
private let urlOutputCHT: String = "\(urlCurrentFolder.path)/data-cht.txt"
|
||||
private let urlOutputCHS: String = "./data-chs.txt"
|
||||
private let urlOutputCHT: String = "./data-cht.txt"
|
||||
|
||||
private let urlJSONSymbols: String = "\(urlCurrentFolder.path)/data-symbols.json"
|
||||
private let urlJSONZhuyinwen: String = "\(urlCurrentFolder.path)/data-zhuyinwen.json"
|
||||
private let urlJSONCNS: String = "\(urlCurrentFolder.path)/data-cns.json"
|
||||
private let urlJSONSymbols: String = "./data-symbols.json"
|
||||
private let urlJSONZhuyinwen: String = "./data-zhuyinwen.json"
|
||||
private let urlJSONCNS: String = "./data-cns.json"
|
||||
|
||||
private let urlJSONCHS: String = "\(urlCurrentFolder.path)/data-chs.json"
|
||||
private let urlJSONCHT: String = "\(urlCurrentFolder.path)/data-cht.json"
|
||||
private let urlJSONBPMFReverseLookup: String = "\(urlCurrentFolder.path)/data-bpmf-reverse-lookup.json"
|
||||
private let urlJSONBPMFReverseLookupCNS1: String = "\(urlCurrentFolder.path)/data-bpmf-reverse-lookup-CNS1.json"
|
||||
private let urlJSONBPMFReverseLookupCNS2: String = "\(urlCurrentFolder.path)/data-bpmf-reverse-lookup-CNS2.json"
|
||||
private let urlJSONBPMFReverseLookupCNS3: String = "\(urlCurrentFolder.path)/data-bpmf-reverse-lookup-CNS3.json"
|
||||
private let urlJSONBPMFReverseLookupCNS4: String = "\(urlCurrentFolder.path)/data-bpmf-reverse-lookup-CNS4.json"
|
||||
private let urlJSONBPMFReverseLookupCNS5: String = "\(urlCurrentFolder.path)/data-bpmf-reverse-lookup-CNS5.json"
|
||||
private let urlJSONBPMFReverseLookupCNS6: String = "\(urlCurrentFolder.path)/data-bpmf-reverse-lookup-CNS6.json"
|
||||
private let urlJSONCHS: String = "./data-chs.json"
|
||||
private let urlJSONCHT: String = "./data-cht.json"
|
||||
private let urlJSONBPMFReverseLookup: String = "./data-bpmf-reverse-lookup.json"
|
||||
private let urlJSONBPMFReverseLookupCNS1: String = "./data-bpmf-reverse-lookup-CNS1.json"
|
||||
private let urlJSONBPMFReverseLookupCNS2: String = "./data-bpmf-reverse-lookup-CNS2.json"
|
||||
private let urlJSONBPMFReverseLookupCNS3: String = "./data-bpmf-reverse-lookup-CNS3.json"
|
||||
private let urlJSONBPMFReverseLookupCNS4: String = "./data-bpmf-reverse-lookup-CNS4.json"
|
||||
private let urlJSONBPMFReverseLookupCNS5: String = "./data-bpmf-reverse-lookup-CNS5.json"
|
||||
private let urlJSONBPMFReverseLookupCNS6: String = "./data-bpmf-reverse-lookup-CNS6.json"
|
||||
|
||||
private var isReverseLookupDictionaryProcessed: Bool = false
|
||||
|
||||
private let urlSQLite: String = "\(urlCurrentFolder.path)/Build/Release/vChewingFactoryDatabase.sqlite"
|
||||
|
||||
private var mapReverseLookupForCheck: [String: [String]] = [:]
|
||||
private var exceptedChars: Set<String> = .init()
|
||||
|
||||
private var ptrSQL: OpaquePointer?
|
||||
|
||||
var rangeMapJSONCHS: [String: [String]] = [:]
|
||||
var rangeMapJSONCHT: [String: [String]] = [:]
|
||||
var rangeMapSymbols: [String: [String]] = [:]
|
||||
var rangeMapZhuyinwen: [String: [String]] = [:]
|
||||
var rangeMapCNS: [String: [String]] = [:]
|
||||
var rangeMapReverseLookup: [String: [String]] = [:]
|
||||
/// Also use mapReverseLookupForCheck.
|
||||
|
||||
// MARK: - 準備資料庫
|
||||
|
||||
func prepareDatabase() -> Bool {
|
||||
let sqlMakeTableMACV = """
|
||||
DROP TABLE IF EXISTS DATA_REV;
|
||||
DROP TABLE IF EXISTS DATA_MAIN;
|
||||
CREATE TABLE IF NOT EXISTS DATA_MAIN (
|
||||
theKey TEXT NOT NULL,
|
||||
theDataCHS TEXT,
|
||||
theDataCHT TEXT,
|
||||
theDataCNS TEXT,
|
||||
theDataMISC TEXT,
|
||||
theDataSYMB TEXT,
|
||||
theDataCHEW TEXT,
|
||||
PRIMARY KEY (theKey)
|
||||
) WITHOUT ROWID;
|
||||
CREATE TABLE IF NOT EXISTS DATA_REV (
|
||||
theChar TEXT NOT NULL,
|
||||
theReadings TEXT NOT NULL,
|
||||
PRIMARY KEY (theChar)
|
||||
) WITHOUT ROWID;
|
||||
"""
|
||||
guard sqlite3_open(":memory:", &ptrSQL) == SQLITE_OK else { return false }
|
||||
guard sqlite3_exec(ptrSQL, "PRAGMA synchronous = OFF;", nil, nil, nil) == SQLITE_OK else { return false }
|
||||
guard sqlite3_exec(ptrSQL, "PRAGMA journal_mode = OFF;", nil, nil, nil) == SQLITE_OK else { return false }
|
||||
guard sqlMakeTableMACV.runAsSQLExec(dbPointer: &ptrSQL) else { return false }
|
||||
guard "begin;".runAsSQLExec(dbPointer: &ptrSQL) else { return false }
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@discardableResult func writeMainMapToSQL(_ theMap: [String: [String]], column columnName: String) -> Bool {
|
||||
for (encryptedKey, arrValues) in theMap {
|
||||
// SQL 語言需要對西文 ASCII 半形單引號做回退處理、變成「''」。
|
||||
let safeKey = encryptedKey.replacingOccurrences(of: "'", with: "''")
|
||||
let valueText = arrValues.joined(separator: "\t").replacingOccurrences(of: "'", with: "''")
|
||||
let sqlStmt = "INSERT INTO DATA_MAIN (theKey, \(columnName)) VALUES ('\(safeKey)', '\(valueText)') ON CONFLICT(theKey) DO UPDATE SET \(columnName)='\(valueText)';"
|
||||
guard sqlStmt.runAsSQLExec(dbPointer: &ptrSQL) else {
|
||||
print("Failed: " + sqlStmt)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@discardableResult func writeRevLookupMapToSQL(_ theMap: [String: [String]]) -> Bool {
|
||||
for (encryptedKey, arrValues) in theMap {
|
||||
// SQL 語言需要對西文 ASCII 半形單引號做回退處理、變成「''」。
|
||||
let safeKey = encryptedKey.replacingOccurrences(of: "'", with: "''")
|
||||
let valueText = arrValues.joined(separator: "\t").replacingOccurrences(of: "'", with: "''")
|
||||
let sqlStmt = "INSERT INTO DATA_REV (theChar, theReadings) VALUES ('\(safeKey)', '\(valueText)') ON CONFLICT(theChar) DO UPDATE SET theReadings='\(valueText)';"
|
||||
guard sqlStmt.runAsSQLExec(dbPointer: &ptrSQL) else {
|
||||
print("Failed: " + sqlStmt)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: - Dump SQLite3 Memory Database to File.
|
||||
|
||||
@discardableResult func dumpSQLDB() -> Bool {
|
||||
var ptrSQLTarget: OpaquePointer?
|
||||
defer { sqlite3_close_v2(ptrSQLTarget) }
|
||||
guard sqlite3_open(urlSQLite, &ptrSQLTarget) == SQLITE_OK else { return false }
|
||||
let ptrBackupObj = sqlite3_backup_init(ptrSQLTarget, "main", ptrSQL, "main")
|
||||
if ptrBackupObj != nil {
|
||||
sqlite3_backup_step(ptrBackupObj, -1)
|
||||
sqlite3_backup_finish(ptrBackupObj)
|
||||
}
|
||||
return sqlite3_errcode(ptrSQLTarget) == SQLITE_OK
|
||||
}
|
||||
|
||||
// MARK: - 載入詞組檔案且輸出陣列
|
||||
|
||||
func rawDictForPhrases(isCHS: Bool) -> [Unigram] {
|
||||
|
@ -413,10 +319,8 @@ func rawDictForKanjis(isCHS: Bool) -> [Unigram] {
|
|||
if !isReverseLookupDictionaryProcessed {
|
||||
do {
|
||||
isReverseLookupDictionaryProcessed = true
|
||||
if compileJSON {
|
||||
try JSONSerialization.data(withJSONObject: mapReverseLookupJSON, options: .sortedKeys).write(
|
||||
to: URL(fileURLWithPath: urlJSONBPMFReverseLookup))
|
||||
}
|
||||
try JSONSerialization.data(withJSONObject: mapReverseLookupJSON, options: .sortedKeys).write(
|
||||
to: URL(fileURLWithPath: urlJSONBPMFReverseLookup))
|
||||
mapReverseLookupForCheck = mapReverseLookupUnencrypted
|
||||
} catch {
|
||||
NSLog(" - Core Reverse Lookup Data Generation Failed.")
|
||||
|
@ -555,8 +459,10 @@ func fileOutput(isCHS: Bool) {
|
|||
let i18n: String = isCHS ? "簡體中文" : "繁體中文"
|
||||
var strPunctuation = ""
|
||||
var rangeMapJSON: [String: [String]] = [:]
|
||||
let pathOutput = URL(fileURLWithPath: isCHS ? urlOutputCHS : urlOutputCHT)
|
||||
let jsonURL = URL(fileURLWithPath: isCHS ? urlJSONCHS : urlJSONCHT)
|
||||
let pathOutput = urlCurrentFolder.appendingPathComponent(
|
||||
isCHS ? urlOutputCHS : urlOutputCHT)
|
||||
let jsonURL = urlCurrentFolder.appendingPathComponent(
|
||||
isCHS ? urlJSONCHS : urlJSONCHT)
|
||||
var strPrintLine = ""
|
||||
// 讀取標點內容
|
||||
do {
|
||||
|
@ -626,18 +532,11 @@ func fileOutput(isCHS: Bool) {
|
|||
NSLog(" - \(i18n): 要寫入檔案的 txt 內容編譯完畢。")
|
||||
do {
|
||||
try strPrintLine.write(to: pathOutput, atomically: true, encoding: .utf8)
|
||||
if compileJSON {
|
||||
try JSONSerialization.data(withJSONObject: rangeMapJSON, options: .sortedKeys).write(to: jsonURL)
|
||||
}
|
||||
if isCHS {
|
||||
rangeMapJSONCHS = rangeMapJSON
|
||||
} else {
|
||||
rangeMapJSONCHT = rangeMapJSON
|
||||
}
|
||||
try JSONSerialization.data(withJSONObject: rangeMapJSON, options: .sortedKeys).write(to: jsonURL)
|
||||
} catch {
|
||||
NSLog(" - \(i18n): Error on writing strings to file: \(error)")
|
||||
}
|
||||
NSLog(" - \(i18n): JSON & TXT 寫入完成。")
|
||||
NSLog(" - \(i18n): 寫入完成。")
|
||||
if !arrFoundedDuplications.isEmpty {
|
||||
NSLog(" - \(i18n): 尋得下述重複項目,請務必手動排查:")
|
||||
print("-------------------")
|
||||
|
@ -677,9 +576,7 @@ func commonFileOutput() {
|
|||
let theKey = String(neta[1])
|
||||
let theValue = String(neta[0])
|
||||
if !neta[0].isEmpty, !neta[1].isEmpty, line.first != "#" {
|
||||
let encryptedKey = cnvPhonabetToASCII(theKey)
|
||||
mapSymbols[encryptedKey, default: []].append(theValue)
|
||||
rangeMapSymbols[encryptedKey, default: []].append(theValue)
|
||||
mapSymbols[cnvPhonabetToASCII(theKey), default: []].append(theValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -690,9 +587,7 @@ func commonFileOutput() {
|
|||
let theKey = String(neta[1])
|
||||
let theValue = String(neta[0])
|
||||
if !neta[0].isEmpty, !neta[1].isEmpty, line.first != "#" {
|
||||
let encryptedKey = cnvPhonabetToASCII(theKey)
|
||||
mapZhuyinwen[encryptedKey, default: []].append(theValue)
|
||||
rangeMapZhuyinwen[encryptedKey, default: []].append(theValue)
|
||||
mapZhuyinwen[cnvPhonabetToASCII(theKey), default: []].append(theValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -703,33 +598,30 @@ func commonFileOutput() {
|
|||
let theKey = String(neta[1])
|
||||
let theValue = String(neta[0])
|
||||
if !neta[0].isEmpty, !neta[1].isEmpty, line.first != "#" {
|
||||
let encryptedKey = cnvPhonabetToASCII(theKey)
|
||||
mapCNS[encryptedKey, default: []].append(theValue)
|
||||
rangeMapCNS[encryptedKey, default: []].append(theValue)
|
||||
mapCNS[cnvPhonabetToASCII(theKey), default: []].append(theValue)
|
||||
json: if !theKey.contains("_"), !theKey.contains("-") {
|
||||
rangeMapReverseLookup[theValue, default: []].append(encryptedKey)
|
||||
if mapReverseLookupCNS1.keys.count <= 16500 {
|
||||
mapReverseLookupCNS1[theValue, default: []].append(encryptedKey)
|
||||
mapReverseLookupCNS1[theValue, default: []].append(cnvPhonabetToASCII(theKey))
|
||||
break json
|
||||
}
|
||||
if mapReverseLookupCNS2.keys.count <= 16500 {
|
||||
mapReverseLookupCNS2[theValue, default: []].append(encryptedKey)
|
||||
mapReverseLookupCNS2[theValue, default: []].append(cnvPhonabetToASCII(theKey))
|
||||
break json
|
||||
}
|
||||
if mapReverseLookupCNS3.keys.count <= 16500 {
|
||||
mapReverseLookupCNS3[theValue, default: []].append(encryptedKey)
|
||||
mapReverseLookupCNS3[theValue, default: []].append(cnvPhonabetToASCII(theKey))
|
||||
break json
|
||||
}
|
||||
if mapReverseLookupCNS4.keys.count <= 16500 {
|
||||
mapReverseLookupCNS4[theValue, default: []].append(encryptedKey)
|
||||
mapReverseLookupCNS4[theValue, default: []].append(cnvPhonabetToASCII(theKey))
|
||||
break json
|
||||
}
|
||||
if mapReverseLookupCNS5.keys.count <= 16500 {
|
||||
mapReverseLookupCNS5[theValue, default: []].append(encryptedKey)
|
||||
mapReverseLookupCNS5[theValue, default: []].append(cnvPhonabetToASCII(theKey))
|
||||
break json
|
||||
}
|
||||
if mapReverseLookupCNS6.keys.count <= 16500 {
|
||||
mapReverseLookupCNS6[theValue, default: []].append(encryptedKey)
|
||||
mapReverseLookupCNS6[theValue, default: []].append(cnvPhonabetToASCII(theKey))
|
||||
break json
|
||||
}
|
||||
}
|
||||
|
@ -738,32 +630,62 @@ func commonFileOutput() {
|
|||
}
|
||||
NSLog(" - \(i18n): 要寫入檔案的內容編譯完畢。")
|
||||
do {
|
||||
if compileJSON {
|
||||
try JSONSerialization.data(withJSONObject: mapSymbols, options: .sortedKeys).write(
|
||||
to: URL(fileURLWithPath: urlJSONSymbols))
|
||||
try JSONSerialization.data(withJSONObject: mapZhuyinwen, options: .sortedKeys).write(
|
||||
to: URL(fileURLWithPath: urlJSONZhuyinwen))
|
||||
try JSONSerialization.data(withJSONObject: mapCNS, options: .sortedKeys).write(
|
||||
to: URL(fileURLWithPath: urlJSONCNS))
|
||||
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS1, options: .sortedKeys).write(
|
||||
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS1))
|
||||
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS2, options: .sortedKeys).write(
|
||||
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS2))
|
||||
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS3, options: .sortedKeys).write(
|
||||
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS3))
|
||||
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS4, options: .sortedKeys).write(
|
||||
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS4))
|
||||
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS5, options: .sortedKeys).write(
|
||||
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS5))
|
||||
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS6, options: .sortedKeys).write(
|
||||
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS6))
|
||||
}
|
||||
try JSONSerialization.data(withJSONObject: mapSymbols, options: .sortedKeys).write(
|
||||
to: URL(fileURLWithPath: urlJSONSymbols))
|
||||
try JSONSerialization.data(withJSONObject: mapZhuyinwen, options: .sortedKeys).write(
|
||||
to: URL(fileURLWithPath: urlJSONZhuyinwen))
|
||||
try JSONSerialization.data(withJSONObject: mapCNS, options: .sortedKeys).write(
|
||||
to: URL(fileURLWithPath: urlJSONCNS))
|
||||
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS1, options: .sortedKeys).write(
|
||||
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS1))
|
||||
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS2, options: .sortedKeys).write(
|
||||
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS2))
|
||||
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS3, options: .sortedKeys).write(
|
||||
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS3))
|
||||
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS4, options: .sortedKeys).write(
|
||||
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS4))
|
||||
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS5, options: .sortedKeys).write(
|
||||
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS5))
|
||||
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS6, options: .sortedKeys).write(
|
||||
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS6))
|
||||
} catch {
|
||||
NSLog(" - \(i18n): Error on writing strings to file: \(error)")
|
||||
}
|
||||
NSLog(" - \(i18n): 寫入完成。")
|
||||
}
|
||||
|
||||
// MARK: - 主執行緒
|
||||
|
||||
func main() {
|
||||
let globalQueue = DispatchQueue.global(qos: .default)
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
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(timeout: .distantFuture)
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
NSLog("// 全部辭典檔案建置完畢。")
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
// MARK: - 辭庫健康狀況檢查專用函式
|
||||
|
||||
func healthCheck(_ data: [Unigram]) -> String {
|
||||
|
@ -1057,107 +979,3 @@ func healthCheck(_ data: [Unigram]) -> String {
|
|||
result += "\n"
|
||||
return result
|
||||
}
|
||||
|
||||
// MARK: - 與主執行緒有關的任務 Flags
|
||||
|
||||
struct TaskFlags: OptionSet {
|
||||
public let rawValue: Int
|
||||
public init(rawValue: Int) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public static let common = TaskFlags(rawValue: 1 << 0)
|
||||
public static let chs = TaskFlags(rawValue: 1 << 1)
|
||||
public static let cht = TaskFlags(rawValue: 1 << 2)
|
||||
}
|
||||
|
||||
// MARK: - 主執行緒
|
||||
|
||||
var compileJSON = false
|
||||
var compileSQLite = true
|
||||
|
||||
func main() {
|
||||
let arguments = CommandLine.arguments.compactMap { $0.lowercased() }
|
||||
let jsonConditionMet = arguments.contains(where: { $0 == "--json" || $0 == "json" })
|
||||
if jsonConditionMet {
|
||||
NSLog("// 接下來準備建置 JSON 格式的原廠辭典,同時生成用來偵錯的 TXT 副產物。")
|
||||
compileJSON = true
|
||||
compileSQLite = false
|
||||
} else {
|
||||
NSLog("// 接下來準備建置 SQLite 格式的原廠辭典,同時生成用來偵錯的 TXT 副產物。")
|
||||
compileJSON = false
|
||||
compileSQLite = true
|
||||
}
|
||||
let prepared = prepareDatabase()
|
||||
if compileSQLite, !prepared {
|
||||
NSLog("// SQLite 資料庫初期化失敗。")
|
||||
exit(-1)
|
||||
}
|
||||
|
||||
var taskFlags: TaskFlags = [.common, .chs, .cht] {
|
||||
didSet {
|
||||
guard taskFlags.isEmpty else { return }
|
||||
NSLog("// 全部 TXT 辭典檔案建置完畢。")
|
||||
if compileJSON {
|
||||
NSLog("// 全部 JSON 辭典檔案建置完畢。")
|
||||
}
|
||||
if compileSQLite, prepared {
|
||||
NSLog("// 開始整合反查資料。")
|
||||
mapReverseLookupForCheck.forEach { key, values in
|
||||
values.reversed().forEach { valueLiteral in
|
||||
let value = cnvPhonabetToASCII(valueLiteral)
|
||||
if !rangeMapReverseLookup[key, default: []].contains(value) {
|
||||
rangeMapReverseLookup[key, default: []].insert(value, at: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
NSLog("// 反查資料整合完畢。")
|
||||
NSLog("// 準備建置 SQL 資料庫。")
|
||||
writeMainMapToSQL(rangeMapJSONCHS, column: "theDataCHS")
|
||||
writeMainMapToSQL(rangeMapJSONCHT, column: "theDataCHT")
|
||||
writeMainMapToSQL(rangeMapSymbols, column: "theDataSYMB")
|
||||
writeMainMapToSQL(rangeMapZhuyinwen, column: "theDataCHEW")
|
||||
writeMainMapToSQL(rangeMapCNS, column: "theDataCNS")
|
||||
writeRevLookupMapToSQL(rangeMapReverseLookup)
|
||||
let committed = "commit;".runAsSQLExec(dbPointer: &ptrSQL)
|
||||
assert(committed)
|
||||
let compressed = "VACUUM;".runAsSQLExec(dbPointer: &ptrSQL)
|
||||
assert(compressed)
|
||||
if !dumpSQLDB() {
|
||||
NSLog("// SQLite 辭典傾印失敗。")
|
||||
} else {
|
||||
NSLog("// 全部 SQLite 辭典檔案建置完畢。")
|
||||
}
|
||||
sqlite3_close_v2(ptrSQL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let globalQueue = DispatchQueue.global(qos: .default)
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
globalQueue.async {
|
||||
NSLog("// 準備編譯符號表情ㄅ文語料檔案。")
|
||||
commonFileOutput()
|
||||
taskFlags.remove(.common)
|
||||
group.leave()
|
||||
}
|
||||
group.enter()
|
||||
globalQueue.async {
|
||||
NSLog("// 準備編譯繁體中文核心語料檔案。")
|
||||
fileOutput(isCHS: false)
|
||||
taskFlags.remove(.cht)
|
||||
group.leave()
|
||||
}
|
||||
group.enter()
|
||||
globalQueue.async {
|
||||
NSLog("// 準備編譯簡體中文核心語料檔案。")
|
||||
fileOutput(isCHS: true)
|
||||
taskFlags.remove(.chs)
|
||||
group.leave()
|
||||
}
|
||||
// 一直等待完成
|
||||
group.wait()
|
||||
}
|
||||
|
||||
main()
|
||||
|
|
|
@ -50,7 +50,7 @@ var allRegisteredInstancesOfThisInputMethod: [TISInputSource] {
|
|||
else {
|
||||
return []
|
||||
}
|
||||
return TISInputSource.match(modeIDs: tsInputModeListKey.keys.map(\.description))
|
||||
return tsInputModeListKey.keys.compactMap { TISInputSource.generate(from: $0) }
|
||||
}
|
||||
|
||||
// MARK: - NSApp Activation Helper
|
||||
|
@ -101,31 +101,31 @@ public enum AlertType: String, Identifiable {
|
|||
|
||||
var title: LocalizedStringKey {
|
||||
switch self {
|
||||
case .nothing: return ""
|
||||
case .installationFailed: return "Install Failed"
|
||||
case .missingAfterRegistration: return "Fatal Error"
|
||||
case .postInstallAttention: return "Attention"
|
||||
case .postInstallWarning: return "Warning"
|
||||
case .postInstallOK: return "Installation Successful"
|
||||
case .nothing: ""
|
||||
case .installationFailed: "Install Failed"
|
||||
case .missingAfterRegistration: "Fatal Error"
|
||||
case .postInstallAttention: "Attention"
|
||||
case .postInstallWarning: "Warning"
|
||||
case .postInstallOK: "Installation Successful"
|
||||
}
|
||||
}
|
||||
|
||||
var message: String {
|
||||
switch self {
|
||||
case .nothing: return ""
|
||||
case .nothing: ""
|
||||
case .installationFailed:
|
||||
return "Cannot copy the file to the destination.".i18n
|
||||
"Cannot copy the file to the destination.".i18n
|
||||
case .missingAfterRegistration:
|
||||
return String(
|
||||
String(
|
||||
format: "Cannot find input source %@ after registration.".i18n,
|
||||
kTISInputSourceID
|
||||
)
|
||||
case .postInstallAttention:
|
||||
return "vChewing is upgraded, but please log out or reboot for the new version to be fully functional.".i18n
|
||||
"vChewing is upgraded, but please log out or reboot for the new version to be fully functional.".i18n
|
||||
case .postInstallWarning:
|
||||
return "Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources.".i18n
|
||||
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources.".i18n
|
||||
case .postInstallOK:
|
||||
return "vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account.".i18n
|
||||
"vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account.".i18n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,8 +10,6 @@ import AppKit
|
|||
import SwiftUI
|
||||
|
||||
public struct MainView: View {
|
||||
static let strCopyrightLabel = Bundle.main.localizedInfoDictionary?["NSHumanReadableCopyright"] as? String ?? "BAD_COPYRIGHT_LABEL"
|
||||
|
||||
@State var pendingSheetPresenting = false
|
||||
@State var isShowingAlertForFailedInstallation = false
|
||||
@State var isShowingAlertForMissingPostInstall = false
|
||||
|
@ -55,7 +53,6 @@ public struct MainView: View {
|
|||
Text("v\(versionString) Build \(installingVersion)").lineLimit(1)
|
||||
}.fixedSize()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -75,12 +72,10 @@ public struct MainView: View {
|
|||
HStack(alignment: .top) {
|
||||
Text("i18n:installer.DISCLAIMER_TEXT")
|
||||
.font(.custom("Tahoma", size: 11))
|
||||
.opacity(0.5)
|
||||
.frame(maxWidth: .infinity)
|
||||
VStack(spacing: 4) {
|
||||
Button { installationButtonClicked() } label: {
|
||||
Text(isUpgrading ? "i18n:installer.DO_APP_UPGRADE" : "i18n:installer.ACCEPT_INSTALLATION")
|
||||
.bold().frame(width: 114)
|
||||
Text(isUpgrading ? "i18n:installer.DO_APP_UPGRADE" : "i18n:installer.ACCEPT_INSTALLATION").frame(width: 114)
|
||||
}
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.disabled(!isCancelButtonEnabled)
|
||||
|
@ -141,13 +136,10 @@ public struct MainView: View {
|
|||
}
|
||||
}
|
||||
// OTHER
|
||||
.padding(12)
|
||||
.padding([.horizontal, .bottom], 12)
|
||||
.frame(width: 533, alignment: .topLeading)
|
||||
.navigationTitle(mainWindowTitle)
|
||||
.fixedSize()
|
||||
.foregroundStyle(Color(nsColor: NSColor.textColor))
|
||||
.background(Color(nsColor: NSColor.windowBackgroundColor))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
.frame(minWidth: 533, idealWidth: 533, maxWidth: 533,
|
||||
minHeight: 386, idealHeight: 386, maxHeight: 386,
|
||||
alignment: .top)
|
||||
|
|
|
@ -1,30 +1,30 @@
|
|||
"Abort" = "Abort";
|
||||
"Attention" = "Attention";
|
||||
"vChewing Input Method" = "vChewing Input Method";
|
||||
"i18n:installer.DO_APP_UPGRADE" = "Accept & 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 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";
|
||||
"Installation Successful" = "Installation Successful";
|
||||
"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.";
|
||||
"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.";
|
||||
"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";
|
||||
"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" = "中止";
|
||||
"Attention" = "ご注意";
|
||||
"vChewing Input Method" = "威注音入力アプリ";
|
||||
"i18n:installer.DO_APP_UPGRADE" = "承認と更新";
|
||||
"Cancel" = "取消";
|
||||
"Cannot activate the input method." = "入力アプリ、起動失敗。";
|
||||
"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" = "実装失敗。";
|
||||
"Installation Successful" = "実装完了";
|
||||
"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このシステムユーザーアカウントで初めて実装した場合、再ログインしてください。";
|
||||
"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." = "威注音入力の更新は実装完了しましたが、うまく作動できるために、このパソコンの再起動および再ログインが必要だと恐れ入ります。";
|
||||
"Fatal Error" = "致命錯乱";
|
||||
"Abort" = "中止";
|
||||
"Cannot register input source %@ at %@." = "「%2$@」で入力アプリ「\"%1$@\"」の実装は失敗しました。";
|
||||
"Cannot find input source %@ after registration." = "登録済みですが「%@」は見つけませんでした。";
|
||||
"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" = "放弃安装";
|
||||
"Attention" = "请注意";
|
||||
"vChewing Input Method" = "威注音输入法";
|
||||
"i18n:installer.DO_APP_UPGRADE" = "接受并升级";
|
||||
"Cancel" = "取消";
|
||||
"Cannot activate the input method." = "无法启用输入法。";
|
||||
"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" = "安装失败";
|
||||
"Installation Successful" = "安装成功";
|
||||
"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若是在當前使用者帳戶內首次安裝的話,請重新登入。";
|
||||
"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 安装完成,但建议您登出或重新开机,以便顺利使用新版。";
|
||||
"Fatal Error" = "安装错误";
|
||||
"Abort" = "放弃安装";
|
||||
"Cannot register input source %@ at %@." = "无法从档案位置 %2$@ 安装输入法 \"%1$@\"。";
|
||||
"Cannot find input source %@ after registration." = "在注册完输入法 \"%@\" 之后仍然无法找到该输入法。";
|
||||
"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" = "放棄安裝";
|
||||
"Attention" = "請注意";
|
||||
"vChewing Input Method" = "威注音輸入法";
|
||||
"i18n:installer.DO_APP_UPGRADE" = "接受並升級";
|
||||
"Cancel" = "取消";
|
||||
"Cannot activate the input method." = "無法啟用輸入法。";
|
||||
"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" = "安裝失敗";
|
||||
"Installation Successful" = "安裝成功";
|
||||
"OK" = "確定";
|
||||
"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…" = "正在試圖結束正在運行的舊版輸入法,大概需要一分鐘…";
|
||||
"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若是在當前使用者帳戶內首次安裝的話,請重新登入。";
|
||||
"Attention" = "請注意";
|
||||
"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" = "安裝不完整";
|
||||
"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
|
||||
|
||||
# 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
|
||||
|
|
|
@ -13,31 +13,12 @@ import SwiftUI
|
|||
struct vChewingInstallerApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ZStack(alignment: .center) {
|
||||
LinearGradient(
|
||||
gradient: Gradient(
|
||||
colors: [
|
||||
Color(red: 0, green: 0, blue: 0xF4 / 255),
|
||||
.black,
|
||||
]
|
||||
),
|
||||
startPoint: .top, endPoint: .bottom
|
||||
).overlay(alignment: .topLeading) {
|
||||
Text("vChewing Input Method")
|
||||
.font(.system(size: 30))
|
||||
.italic().bold()
|
||||
.padding()
|
||||
.foregroundStyle(Color.white)
|
||||
.shadow(color: .black, radius: 0, x: 5, y: 5)
|
||||
}
|
||||
MainView()
|
||||
.shadow(color: .black, radius: 3, x: 0, y: 0)
|
||||
}.frame(width: 1000, height: 630)
|
||||
MainView()
|
||||
.onAppear {
|
||||
NSWindow.allowsAutomaticWindowTabbing = false
|
||||
NSApp.windows.forEach { window in
|
||||
window.titlebarAppearsTransparent = true
|
||||
window.setContentSize(.init(width: 1000, height: 630))
|
||||
window.setContentSize(.init(width: 533, height: 386))
|
||||
window.standardWindowButton(.closeButton)?.isHidden = true
|
||||
window.standardWindowButton(.miniaturizeButton)?.isHidden = true
|
||||
window.standardWindowButton(.zoomButton)?.isHidden = 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
|
||||
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:
|
||||
xcodebuild -project vChewing.xcodeproj -scheme vChewingInstaller -configuration Release $(BUILD_SETTINGS) build
|
||||
|
||||
|
@ -60,7 +42,6 @@ install-release: permission-check
|
|||
.PHONY: clean
|
||||
|
||||
clean:
|
||||
make clean --file=./Packages/Makefile || true
|
||||
xcodebuild -scheme vChewingInstaller -configuration Debug $(BUILD_SETTINGS) clean
|
||||
xcodebuild -scheme vChewingInstaller -configuration Release $(BUILD_SETTINGS) clean
|
||||
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: [
|
||||
.package(path: "../vChewing_OSFrameworkImpl"),
|
||||
.package(path: "../vChewing_CocoaExtension"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "NSAttributedTextView",
|
||||
dependencies: [
|
||||
.product(name: "OSFrameworkImpl", package: "vChewing_OSFrameworkImpl"),
|
||||
.product(name: "CocoaExtension", package: "vChewing_CocoaExtension"),
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
// Modified by The vChewing Project in order to use it with AppKit.
|
||||
|
||||
import AppKit
|
||||
import OSFrameworkImpl
|
||||
import CocoaExtension
|
||||
import SwiftUI
|
||||
|
||||
@available(macOS 10.15, *)
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import AppKit
|
||||
import CocoaExtension
|
||||
import Foundation
|
||||
@testable import NSAttributedTextView
|
||||
import OSFrameworkImpl
|
||||
import Shared
|
||||
import XCTest
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import PackageDescription
|
|||
let package = Package(
|
||||
name: "BookmarkManager",
|
||||
platforms: [
|
||||
.macOS(.v10_13),
|
||||
.macOS(.v11),
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
|
|
|
@ -14,17 +14,14 @@ public class BookmarkManager {
|
|||
return
|
||||
}
|
||||
|
||||
do {
|
||||
var data: Data?
|
||||
if #unavailable(macOS 10.13) {
|
||||
data = NSKeyedArchiver.archivedData(withRootObject: bookmarkDic)
|
||||
} else {
|
||||
data = try NSKeyedArchiver.archivedData(withRootObject: bookmarkDic, requiringSecureCoding: false)
|
||||
if #available(macOS 10.13, *) {
|
||||
do {
|
||||
let data = try NSKeyedArchiver.archivedData(withRootObject: bookmarkDic, requiringSecureCoding: false)
|
||||
try data.write(to: bookmarkURL)
|
||||
NSLog("Did save data to url")
|
||||
} catch {
|
||||
NSLog("Couldn't save bookmarks")
|
||||
}
|
||||
try data?.write(to: bookmarkURL)
|
||||
NSLog("Did save data to url")
|
||||
} catch {
|
||||
NSLog("Couldn't save bookmarks")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -37,24 +34,8 @@ public class BookmarkManager {
|
|||
if fileExists(url) {
|
||||
do {
|
||||
let fileData = try Data(contentsOf: url)
|
||||
if #available(macOS 11.0, *) {
|
||||
if let fileBookmarks = try NSKeyedUnarchiver.unarchivedDictionary(ofKeyClass: NSURL.self, objectClass: NSData.self, from: fileData) as [URL: Data]? {
|
||||
for bookmark in fileBookmarks {
|
||||
restoreBookmark(key: bookmark.key, value: bookmark.value)
|
||||
}
|
||||
}
|
||||
} else if #available(macOS 10.11, *) {
|
||||
if let fileBookmarks = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(fileData) as! [URL: Data]? {
|
||||
for bookmark in fileBookmarks {
|
||||
restoreBookmark(key: bookmark.key, value: bookmark.value)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let fileBookmarks = NSKeyedUnarchiver.unarchiveObject(with: fileData) as! [URL: Data]? {
|
||||
for bookmark in fileBookmarks {
|
||||
restoreBookmark(key: bookmark.key, value: bookmark.value)
|
||||
}
|
||||
}
|
||||
try (NSKeyedUnarchiver.unarchivedObject(ofClass: NSDictionary.self, from: fileData) as? [URL: Data])?.forEach { bookmark in
|
||||
restoreBookmark(key: bookmark.key, value: bookmark.value)
|
||||
}
|
||||
} catch {
|
||||
NSLog("Couldn't load bookmarks")
|
||||
|
|
|
@ -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
|
|
@ -37,7 +37,7 @@ public struct ShiftKeyUpChecker {
|
|||
|
||||
/// 實現邏輯基本上是相同的,只是威注音這邊的行文風格習慣可能與業火五筆有不同。
|
||||
|
||||
private let delayInterval = 0.2
|
||||
private let delayInterval = 0.3
|
||||
private var previousKeyCode: UInt16?
|
||||
private var lastTime: Date = .init()
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// (c) 2019 and onwards Robert Muckle-Jones (Apache 2.0 License).
|
||||
|
||||
import Foundation
|
||||
import SwiftExtension
|
||||
|
||||
public class LineReader {
|
||||
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 unifiedCharDimension: Double { ceil(unifiedSize * 1.0125 + 7) }
|
||||
public static var unifiedTextHeight: Double { ceil(unifiedSize * 19 / 16) }
|
||||
static var internalPrefs = PrefMgr()
|
||||
public var selectionKey: String
|
||||
public let displayedText: String
|
||||
public private(set) var textDimension: NSSize
|
||||
|
@ -82,8 +81,7 @@ public class CandidateCellData: Hashable {
|
|||
}
|
||||
|
||||
public func cellLength(isMatrix: Bool = true) -> Double {
|
||||
let factor: CGFloat = (Self.internalPrefs.minCellWidthForHorizontalMatrix == 0) ? 1.5 : 2
|
||||
let minLength = ceil(Self.unifiedCharDimension * factor + size * 1.25)
|
||||
let minLength = ceil(Self.unifiedCharDimension * 2 + size * 1.25)
|
||||
if displayedText.count <= 2, isMatrix { return minLength }
|
||||
return textDimension.width
|
||||
}
|
||||
|
@ -95,7 +93,7 @@ public class CandidateCellData: Hashable {
|
|||
if #available(macOS 10.15, *) {
|
||||
return NSFont.monospacedSystemFont(ofSize: fontSizeKey, weight: .regular)
|
||||
}
|
||||
return NSFont(name: "Courier New", size: size) ?? phraseFont(size: size)
|
||||
return NSFont(name: "Menlo", size: size) ?? phraseFont(size: size)
|
||||
}
|
||||
|
||||
func phraseFont(size: CGFloat? = nil) -> NSFont {
|
||||
|
@ -202,14 +200,14 @@ public class CandidateCellData: Hashable {
|
|||
return attrStrCandidate
|
||||
}
|
||||
|
||||
public func charDescriptions(shortened: Bool = false) -> [String] {
|
||||
public var charDescriptions: [String] {
|
||||
var result = displayedText
|
||||
if displayedText.contains("("), displayedText.count > 2 {
|
||||
result = displayedText.replacingOccurrences(of: "(", with: "").replacingOccurrences(of: ")", with: "")
|
||||
}
|
||||
return result.flatMap(\.unicodeScalars).compactMap {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -185,7 +185,7 @@ public extension CandidatePool {
|
|||
guard !candidatesShown.filter(\.isHighlighted).isEmpty else { return }
|
||||
isExpanded = true
|
||||
if candidateLines.count <= _maxLinesPerPage {
|
||||
recordedLineRangeForCurrentPage = lineRangeForFirstPage
|
||||
recordedLineRangeForCurrentPage = max(0, currentLineNumber - _maxLinesPerPage + 1) ..< currentLineNumber + 1
|
||||
} else {
|
||||
switch isBackward {
|
||||
case true:
|
||||
|
|
|
@ -301,12 +301,10 @@ extension CandidatePool {
|
|||
.font: Self.blankCell.phraseFont(size: reverseLookupTextSize),
|
||||
]
|
||||
let result = NSMutableAttributedString(string: "", attributes: attrReverseLookupSpacer)
|
||||
var addedCounter = 0
|
||||
for neta in reverseLookupResult {
|
||||
result.append(NSAttributedString(string: " ", attributes: attrReverseLookupSpacer))
|
||||
result.append(NSAttributedString(string: " \(neta) ", attributes: attrReverseLookup))
|
||||
addedCounter += 1
|
||||
if maxLinesPerPage == 1, addedCounter == 2 { break }
|
||||
if maxLinesPerPage == 1 { break }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -17,24 +17,26 @@ open class CtlCandidate: NSWindowController, CtlCandidateProtocol {
|
|||
open var reverseLookupResult: [String] = []
|
||||
|
||||
open func highlightedColor() -> NSColor {
|
||||
var result = NSColor.clear
|
||||
if #available(macOS 10.14, *) {
|
||||
result = .controlAccentColor
|
||||
} else {
|
||||
result = .alternateSelectedControlTextColor
|
||||
var result = NSColor.controlAccentColor
|
||||
var colorBlendAmount: Double = NSApplication.isDarkMode ? 0.3 : 0.0
|
||||
if #available(macOS 10.14, *), !NSApplication.isDarkMode, locale == "zh-Hant" {
|
||||
colorBlendAmount = 0.15
|
||||
}
|
||||
let colorBlendAmount = 0.3
|
||||
// 設定當前高亮候選字的背景顏色。
|
||||
switch locale {
|
||||
case "zh-Hans":
|
||||
result = NSColor.red
|
||||
result = NSColor.systemRed
|
||||
case "zh-Hant":
|
||||
result = NSColor.blue
|
||||
result = NSColor.systemBlue
|
||||
case "ja":
|
||||
result = NSColor.brown
|
||||
result = NSColor.systemBrown
|
||||
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)!
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import AppKit
|
||||
import OSFrameworkImpl
|
||||
import CocoaExtension
|
||||
import Shared
|
||||
|
||||
private extension NSUserInterfaceLayoutOrientation {
|
||||
|
@ -24,19 +24,12 @@ private extension NSUserInterfaceLayoutOrientation {
|
|||
}
|
||||
|
||||
public class CtlCandidateTDK: CtlCandidate, NSWindowDelegate {
|
||||
@objc var observation: NSKeyValueObservation?
|
||||
public var maxLinesPerPage: Int = 0
|
||||
public var useCocoa: Bool = false
|
||||
public var useMouseScrolling: Bool = true
|
||||
private static var thePool: CandidatePool = .init(candidates: [])
|
||||
private static var currentView: NSView = .init()
|
||||
|
||||
public static var currentMenu: NSMenu? {
|
||||
willSet {
|
||||
currentMenu?.cancelTracking()
|
||||
}
|
||||
}
|
||||
|
||||
public static var currentMenu: NSMenu?
|
||||
public static var currentWindow: NSWindow? {
|
||||
willSet {
|
||||
currentWindow?.orderOut(nil)
|
||||
|
@ -79,10 +72,6 @@ public class CtlCandidateTDK: CtlCandidate, NSWindowDelegate {
|
|||
Self.currentWindow = panel
|
||||
window?.delegate = self
|
||||
currentLayout = layout
|
||||
|
||||
observation = Broadcaster.shared.observe(\.eventForClosingAllPanels, options: [.new]) { _, _ in
|
||||
self.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
|
@ -108,34 +97,16 @@ public class CtlCandidateTDK: CtlCandidate, NSWindowDelegate {
|
|||
|
||||
override open func updateDisplay() {
|
||||
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
|
||||
guard let self = self else { return }
|
||||
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) {
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import AppKit
|
||||
import OSFrameworkImpl
|
||||
import Shared
|
||||
|
||||
/// 田所選字窗的 AppKit 简单版本,繪製效率不受 SwiftUI 的限制。
|
||||
|
@ -76,9 +75,7 @@ public extension VwrCandidateTDKAppKit {
|
|||
let allCells = thePool.candidateLines[thePool.lineRangeForCurrentPage].flatMap { $0 }
|
||||
allCells.forEach { currentCell in
|
||||
if currentCell.isHighlighted, !cellHighlightedDrawn {
|
||||
let alphaRatio = NSApplication.isDarkMode ? 0.75 : 1
|
||||
let themeColor = controller?.delegate?.clientAccentColor?.withAlphaComponent(alphaRatio)
|
||||
(themeColor ?? currentCell.themeColorCocoa).setFill()
|
||||
currentCell.themeColorCocoa.setFill()
|
||||
NSBezierPath(roundedRect: sizesCalculated.highlightedCandidate, xRadius: cellRadius, yRadius: cellRadius).fill()
|
||||
cellHighlightedDrawn = true
|
||||
}
|
||||
|
@ -152,17 +149,30 @@ public extension VwrCandidateTDKAppKit {
|
|||
private extension VwrCandidateTDKAppKit {
|
||||
private func prepareMenu() {
|
||||
let newMenu = NSMenu()
|
||||
newMenu.appendItems(self) {
|
||||
NSMenu.Item(
|
||||
verbatim: "↑ \(clickedCell.displayedText)"
|
||||
)?.act(#selector(menuActionOfBoosting(_:)))
|
||||
NSMenu.Item(
|
||||
verbatim: "↓ \(clickedCell.displayedText)"
|
||||
)?.act(#selector(menuActionOfNerfing(_:)))
|
||||
NSMenu.Item(
|
||||
verbatim: "✖︎ \(clickedCell.displayedText)"
|
||||
)?.act(#selector(menuActionOfFiltering(_:)))
|
||||
.nulled(!thePool.isFilterable(target: clickedCell.index))
|
||||
let boostMenuItem = NSMenuItem(
|
||||
title: "↑ \(clickedCell.displayedText)",
|
||||
action: #selector(menuActionOfBoosting(_:)),
|
||||
keyEquivalent: ""
|
||||
)
|
||||
boostMenuItem.target = self
|
||||
newMenu.addItem(boostMenuItem)
|
||||
|
||||
let nerfMenuItem = NSMenuItem(
|
||||
title: "↓ \(clickedCell.displayedText)",
|
||||
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
|
||||
|
@ -198,7 +208,16 @@ private extension VwrCandidateTDKAppKit {
|
|||
|
||||
private extension VwrCandidateTDKAppKit {
|
||||
private func lineBackground(isCurrentLine: Bool, isMatrix: Bool) -> NSColor {
|
||||
(isCurrentLine && isMatrix) ? (NSApplication.isDarkMode ? .controlTextColor.withAlphaComponent(0.05) : .white) : .clear
|
||||
if !isCurrentLine { return .clear }
|
||||
let absBg: NSColor = NSApplication.isDarkMode ? .black : .white
|
||||
switch thePool.layout {
|
||||
case .horizontal where isMatrix:
|
||||
return NSApplication.isDarkMode ? .controlTextColor.withAlphaComponent(0.05) : .white
|
||||
case .vertical where isMatrix:
|
||||
return absBg.withAlphaComponent(0.9)
|
||||
default:
|
||||
return .clear
|
||||
}
|
||||
}
|
||||
|
||||
private var finalContainerOrientation: NSUserInterfaceLayoutOrientation {
|
||||
|
|
|
@ -2,23 +2,25 @@
|
|||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "OSFrameworkImpl",
|
||||
name: "CocoaExtension",
|
||||
platforms: [
|
||||
.macOS(.v11),
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
name: "OSFrameworkImpl",
|
||||
targets: ["OSFrameworkImpl"]
|
||||
name: "CocoaExtension",
|
||||
targets: ["CocoaExtension"]
|
||||
),
|
||||
],
|
||||
dependencies: [
|
||||
.package(path: "../vChewing_IMKUtils"),
|
||||
.package(path: "../vChewing_SwiftExtension"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "OSFrameworkImpl",
|
||||
name: "CocoaExtension",
|
||||
dependencies: [
|
||||
.product(name: "IMKUtils", package: "vChewing_IMKUtils"),
|
||||
.product(name: "SwiftExtension", package: "vChewing_SwiftExtension"),
|
||||
]
|
||||
),
|
|
@ -1,4 +1,4 @@
|
|||
# OSFrameworkImpl
|
||||
# CocoaExtension
|
||||
|
||||
威注音輸入法針對 Cocoa 的一些功能擴充,使程式維護體驗更佳。
|
||||
|
|
@ -71,18 +71,6 @@ public extension NSAttributedString {
|
|||
|
||||
public extension NSString {
|
||||
var localized: String { NSLocalizedString(description, comment: "") }
|
||||
|
||||
@objc func getCharDescriptions(_: Any? = nil) -> [String] {
|
||||
(self as String).charDescriptions
|
||||
}
|
||||
|
||||
@objc func getCodePoints(_: Any? = nil) -> [String] {
|
||||
(self as String).codePoints
|
||||
}
|
||||
|
||||
@objc func getDescriptionAsCodePoints(_: Any? = nil) -> [String] {
|
||||
(self as String).describedAsCodePoints
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - NSRange Extension
|
||||
|
@ -140,14 +128,15 @@ public extension NSApplication {
|
|||
// MARK: - System Dark Mode Status Detector.
|
||||
|
||||
static var isDarkMode: Bool {
|
||||
// "NSApp" can be nil during SPM unit tests.
|
||||
// Therefore, the method dedicated for macOS 10.15 and later is not considered stable anymore.
|
||||
// Fortunately, the method for macOS 10.14 works well on later macOS releases.
|
||||
if #available(macOS 10.14, *), let strAIS = UserDefaults.current.string(forKey: "AppleInterfaceStyle") {
|
||||
return strAIS.lowercased().contains("dark")
|
||||
} else {
|
||||
return false
|
||||
if #unavailable(macOS 10.14) { return false }
|
||||
if #available(macOS 10.15, *) {
|
||||
let appearanceDescription = NSApp.effectiveAppearance.debugDescription
|
||||
.lowercased()
|
||||
return appearanceDescription.contains("dark")
|
||||
} else if let appleInterfaceStyle = UserDefaults.current.string(forKey: "AppleInterfaceStyle") {
|
||||
return appleInterfaceStyle.lowercased().contains("dark")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
||||
public extension Date {
|
||||
|
@ -282,56 +284,3 @@ public extension NSApplication {
|
|||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Reading bundle's accent color.
|
||||
|
||||
public extension NSColor {
|
||||
static var accentColor: NSColor {
|
||||
guard #unavailable(macOS 10.14) else { return .controlAccentColor }
|
||||
return .alternateSelectedControlColor
|
||||
}
|
||||
}
|
||||
|
||||
public extension Bundle {
|
||||
func getAccentColor() -> NSColor {
|
||||
let defaultResult: NSColor = .accentColor
|
||||
let queryPhrase = localizedInfoDictionary?["NSAccentColorName"] as? String ?? infoDictionary?["NSAccentColorName"] as? String
|
||||
guard let queryPhrase = queryPhrase, !queryPhrase.isEmpty else { return defaultResult }
|
||||
guard #available(macOS 10.13, *) else { return defaultResult }
|
||||
return NSColor(named: queryPhrase, bundle: self) ?? defaultResult
|
||||
}
|
||||
}
|
||||
|
||||
public extension NSRunningApplication {
|
||||
private static var temporatyBundlePtr: Bundle?
|
||||
|
||||
static func findAccentColor(with bundleIdentifier: String) -> NSColor {
|
||||
let matchedRunningApps = Self.runningApplications(withBundleIdentifier: bundleIdentifier)
|
||||
guard let matchedAppURL = matchedRunningApps.first?.bundleURL else { return .accentColor }
|
||||
Self.temporatyBundlePtr = Bundle(url: matchedAppURL)
|
||||
defer { temporatyBundlePtr = nil }
|
||||
let bundleColor = Self.temporatyBundlePtr?.getAccentColor().usingColorSpace(.deviceRGB)
|
||||
guard let bundleColor = bundleColor else { return .accentColor }
|
||||
let h = bundleColor.hueComponent
|
||||
let s = bundleColor.saturationComponent
|
||||
return .init(hue: h, saturation: s, brightness: 128, alpha: 1)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Check whether system's accent color is fixed with non-default value.
|
||||
|
||||
public extension NSApplication {
|
||||
var isAccentColorCustomized: Bool {
|
||||
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")
|
||||
}
|
|
@ -6,103 +6,42 @@
|
|||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
import IMKUtils
|
||||
|
||||
public struct KBEvent: InputSignalProtocol, Hashable {
|
||||
public private(set) var type: EventType
|
||||
public private(set) var modifierFlags: ModifierFlags
|
||||
public private(set) var timestamp: TimeInterval
|
||||
public private(set) var windowNumber: Int
|
||||
public private(set) var characters: String?
|
||||
public private(set) var charactersIgnoringModifiers: String?
|
||||
public private(set) var isARepeat: Bool
|
||||
public private(set) var keyCode: UInt16
|
||||
// MARK: - NSEvent Extension - Reconstructors
|
||||
|
||||
public init(
|
||||
with type: KBEvent.EventType? = nil,
|
||||
modifierFlags: KBEvent.ModifierFlags? = nil,
|
||||
public extension NSEvent {
|
||||
func reinitiate(
|
||||
with type: NSEvent.EventType? = nil,
|
||||
location: NSPoint? = nil,
|
||||
modifierFlags: NSEvent.ModifierFlags? = nil,
|
||||
timestamp: TimeInterval? = nil,
|
||||
windowNumber: Int? = nil,
|
||||
characters: String? = nil,
|
||||
charactersIgnoringModifiers: String? = nil,
|
||||
isARepeat: Bool? = nil,
|
||||
keyCode: UInt16? = nil
|
||||
) {
|
||||
var characters = characters
|
||||
checkSpecialKey: if let matchedKey = KeyCode(rawValue: keyCode ?? 0), let flags = modifierFlags {
|
||||
let scalar = matchedKey.correspondedSpecialKeyScalar(flags: flags)
|
||||
guard let scalar = scalar else { break checkSpecialKey }
|
||||
characters = .init(scalar)
|
||||
}
|
||||
self.type = type ?? .keyDown
|
||||
self.modifierFlags = modifierFlags ?? []
|
||||
self.timestamp = timestamp ?? Date().timeIntervalSince1970
|
||||
self.windowNumber = windowNumber ?? 0
|
||||
self.characters = characters ?? ""
|
||||
self.charactersIgnoringModifiers = charactersIgnoringModifiers ?? characters ?? ""
|
||||
self.isARepeat = isARepeat ?? false
|
||||
self.keyCode = keyCode ?? KeyCode.kNone.rawValue
|
||||
}
|
||||
|
||||
public func reinitiate(
|
||||
with type: KBEvent.EventType? = nil,
|
||||
modifierFlags: KBEvent.ModifierFlags? = nil,
|
||||
timestamp: TimeInterval? = nil,
|
||||
windowNumber: Int? = nil,
|
||||
characters: String? = nil,
|
||||
charactersIgnoringModifiers: String? = nil,
|
||||
isARepeat: Bool? = nil,
|
||||
keyCode: UInt16? = nil
|
||||
) -> KBEvent {
|
||||
) -> NSEvent? {
|
||||
let oldChars: String = text
|
||||
return KBEvent(
|
||||
with: type ?? .keyDown,
|
||||
return NSEvent.keyEvent(
|
||||
with: type ?? self.type,
|
||||
location: location ?? locationInWindow,
|
||||
modifierFlags: modifierFlags ?? self.modifierFlags,
|
||||
timestamp: timestamp ?? self.timestamp,
|
||||
windowNumber: windowNumber ?? self.windowNumber,
|
||||
context: nil,
|
||||
characters: characters ?? oldChars,
|
||||
charactersIgnoringModifiers: charactersIgnoringModifiers ?? characters ?? oldChars,
|
||||
isARepeat: isARepeat ?? self.isARepeat,
|
||||
keyCode: keyCode ?? self.keyCode
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - KBEvent Extension - SubTypes
|
||||
|
||||
public extension KBEvent {
|
||||
struct ModifierFlags: OptionSet, Hashable {
|
||||
public init(rawValue: UInt) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public let rawValue: UInt
|
||||
public static let capsLock = ModifierFlags(rawValue: 1 << 16) // Set if Caps Lock key is pressed.
|
||||
public static let shift = ModifierFlags(rawValue: 1 << 17) // Set if Shift key is pressed.
|
||||
public static let control = ModifierFlags(rawValue: 1 << 18) // Set if Control key is pressed.
|
||||
public static let option = ModifierFlags(rawValue: 1 << 19) // Set if Option or Alternate key is pressed.
|
||||
public static let command = ModifierFlags(rawValue: 1 << 20) // Set if Command key is pressed.
|
||||
public static let numericPad = ModifierFlags(rawValue: 1 << 21) // Set if any key in the numeric keypad is pressed.
|
||||
public static let help = ModifierFlags(rawValue: 1 << 22) // Set if the Help key is pressed.
|
||||
public static let function = ModifierFlags(rawValue: 1 << 23) // Set if any function key is pressed.
|
||||
public static let deviceIndependentFlagsMask = ModifierFlags(rawValue: 0xFFFF_0000)
|
||||
}
|
||||
|
||||
enum EventType: UInt8 {
|
||||
case keyDown = 10
|
||||
case keyUp = 11
|
||||
case flagsChanged = 12
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - KBEvent Extension - Emacs Key Conversions
|
||||
|
||||
public extension KBEvent {
|
||||
/// 自 Emacs 熱鍵的 KBEvent 翻譯回標準 KBEvent。失敗的話則會返回原始 KBEvent 自身。
|
||||
/// 自 Emacs 熱鍵的 NSEvent 翻譯回標準 NSEvent。失敗的話則會返回原始 NSEvent 自身。
|
||||
/// - Parameter isVerticalTyping: 是否按照縱排來操作。
|
||||
/// - Returns: 翻譯結果。失敗的話則返回翻譯原文。
|
||||
func convertFromEmacsKeyEvent(isVerticalContext: Bool) -> KBEvent {
|
||||
func convertFromEmacsKeyEvent(isVerticalContext: Bool) -> NSEvent {
|
||||
guard isEmacsKey else { return self }
|
||||
let newKeyCode: UInt16 = {
|
||||
switch isVerticalContext {
|
||||
|
@ -111,15 +50,32 @@ public extension KBEvent {
|
|||
}
|
||||
}()
|
||||
guard newKeyCode != 0 else { return self }
|
||||
return reinitiate(modifierFlags: [], characters: nil, charactersIgnoringModifiers: nil, keyCode: newKeyCode)
|
||||
let newCharScalar: Unicode.Scalar = {
|
||||
switch charCode {
|
||||
case 6:
|
||||
return isVerticalContext
|
||||
? NSEvent.SpecialKey.downArrow.unicodeScalar : NSEvent.SpecialKey.rightArrow.unicodeScalar
|
||||
case 2:
|
||||
return isVerticalContext
|
||||
? NSEvent.SpecialKey.upArrow.unicodeScalar : NSEvent.SpecialKey.leftArrow.unicodeScalar
|
||||
case 1: return NSEvent.SpecialKey.home.unicodeScalar
|
||||
case 5: return NSEvent.SpecialKey.end.unicodeScalar
|
||||
case 4: return NSEvent.SpecialKey.deleteForward.unicodeScalar // Use "deleteForward" for PC delete.
|
||||
case 22: return NSEvent.SpecialKey.pageDown.unicodeScalar
|
||||
default: return .init(0)
|
||||
}
|
||||
}()
|
||||
let newChar = String(newCharScalar)
|
||||
return reinitiate(modifierFlags: [], characters: newChar, charactersIgnoringModifiers: newChar, keyCode: newKeyCode)
|
||||
?? self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - KBEvent Extension - InputSignalProtocol
|
||||
// MARK: - NSEvent Extension - InputSignalProtocol
|
||||
|
||||
public extension KBEvent {
|
||||
public extension NSEvent {
|
||||
var isTypingVertical: Bool { charactersIgnoringModifiers == "Vertical" }
|
||||
/// KBEvent.characters 的類型安全版。
|
||||
/// NSEvent.characters 的類型安全版。
|
||||
/// - Remark: 注意:必須針對 event.type == .flagsChanged 提前返回結果,
|
||||
/// 否則,每次處理這種判斷時都會因為讀取 event.characters? 而觸發 NSInternalInconsistencyException。
|
||||
var text: String { isFlagChanged ? "" : characters ?? "" }
|
||||
|
@ -142,6 +98,10 @@ public extension KBEvent {
|
|||
modifierFlags.intersection(.deviceIndependentFlagsMask).subtracting(.capsLock)
|
||||
}
|
||||
|
||||
static var keyModifierFlags: ModifierFlags {
|
||||
Self.modifierFlags.intersection(.deviceIndependentFlagsMask).subtracting(.capsLock)
|
||||
}
|
||||
|
||||
var isFlagChanged: Bool { type == .flagsChanged }
|
||||
|
||||
var isEmacsKey: Bool {
|
||||
|
@ -244,94 +204,6 @@ public extension KBEvent {
|
|||
|
||||
// MARK: - Enums of Constants
|
||||
|
||||
public extension KBEvent {
|
||||
enum SpecialKey: UInt16 {
|
||||
var unicodeScalar: Unicode.Scalar { .init(rawValue) ?? .init(0) }
|
||||
case upArrow = 0xF700
|
||||
case downArrow = 0xF701
|
||||
case leftArrow = 0xF702
|
||||
case rightArrow = 0xF703
|
||||
case f1 = 0xF704
|
||||
case f2 = 0xF705
|
||||
case f3 = 0xF706
|
||||
case f4 = 0xF707
|
||||
case f5 = 0xF708
|
||||
case f6 = 0xF709
|
||||
case f7 = 0xF70A
|
||||
case f8 = 0xF70B
|
||||
case f9 = 0xF70C
|
||||
case f10 = 0xF70D
|
||||
case f11 = 0xF70E
|
||||
case f12 = 0xF70F
|
||||
case f13 = 0xF710
|
||||
case f14 = 0xF711
|
||||
case f15 = 0xF712
|
||||
case f16 = 0xF713
|
||||
case f17 = 0xF714
|
||||
case f18 = 0xF715
|
||||
case f19 = 0xF716
|
||||
case f20 = 0xF717
|
||||
case f21 = 0xF718
|
||||
case f22 = 0xF719
|
||||
case f23 = 0xF71A
|
||||
case f24 = 0xF71B
|
||||
case f25 = 0xF71C
|
||||
case f26 = 0xF71D
|
||||
case f27 = 0xF71E
|
||||
case f28 = 0xF71F
|
||||
case f29 = 0xF720
|
||||
case f30 = 0xF721
|
||||
case f31 = 0xF722
|
||||
case f32 = 0xF723
|
||||
case f33 = 0xF724
|
||||
case f34 = 0xF725
|
||||
case f35 = 0xF726
|
||||
case insert = 0xF727
|
||||
case deleteForward = 0xF728
|
||||
case home = 0xF729
|
||||
case begin = 0xF72A
|
||||
case end = 0xF72B
|
||||
case pageUp = 0xF72C
|
||||
case pageDown = 0xF72D
|
||||
case printScreen = 0xF72E
|
||||
case scrollLock = 0xF72F
|
||||
case pause = 0xF730
|
||||
case sysReq = 0xF731
|
||||
case `break` = 0xF732
|
||||
case reset = 0xF733
|
||||
case stop = 0xF734
|
||||
case menu = 0xF735
|
||||
case user = 0xF736
|
||||
case system = 0xF737
|
||||
case print = 0xF738
|
||||
case clearLine = 0xF739
|
||||
case clearDisplay = 0xF73A
|
||||
case insertLine = 0xF73B
|
||||
case deleteLine = 0xF73C
|
||||
case insertCharacter = 0xF73D
|
||||
case deleteCharacter = 0xF73E
|
||||
case prev = 0xF73F
|
||||
case next = 0xF740
|
||||
case select = 0xF741
|
||||
case execute = 0xF742
|
||||
case undo = 0xF743
|
||||
case redo = 0xF744
|
||||
case find = 0xF745
|
||||
case help = 0xF746
|
||||
case modeSwitch = 0xF747
|
||||
case enter = 0x03
|
||||
case backspace = 0x08
|
||||
case tab = 0x09
|
||||
case newline = 0x0A
|
||||
case formFeed = 0x0C
|
||||
case carriageReturn = 0x0D
|
||||
case backTab = 0x19
|
||||
case delete = 0x7F
|
||||
case lineSeparator = 0x2028
|
||||
case paragraphSeparator = 0x2029
|
||||
}
|
||||
}
|
||||
|
||||
// Use KeyCodes as much as possible since its recognition won't be affected by macOS Base Keyboard Layouts.
|
||||
// KeyCodes: https://eastmanreference.com/complete-list-of-applescript-key-codes
|
||||
// Also: HIToolbox.framework/Versions/A/Headers/Events.h
|
||||
|
@ -393,79 +265,14 @@ public enum KeyCode: UInt16 {
|
|||
case kDownArrow = 125
|
||||
case kUpArrow = 126
|
||||
|
||||
public func toKBEvent() -> KBEvent {
|
||||
.init(
|
||||
modifierFlags: [],
|
||||
timestamp: TimeInterval(), windowNumber: 0,
|
||||
public func toEvent() -> NSEvent? {
|
||||
NSEvent.keyEvent(
|
||||
with: .keyDown, location: .zero, modifierFlags: [],
|
||||
timestamp: TimeInterval(), windowNumber: 0, context: nil,
|
||||
characters: "", charactersIgnoringModifiers: "",
|
||||
isARepeat: false, keyCode: rawValue
|
||||
)
|
||||
}
|
||||
|
||||
public func correspondedSpecialKeyScalar(flags: KBEvent.ModifierFlags) -> Unicode.Scalar? {
|
||||
var rawData: KBEvent.SpecialKey? {
|
||||
switch self {
|
||||
case .kNone: return nil
|
||||
case .kCarriageReturn: return .carriageReturn
|
||||
case .kTab:
|
||||
return flags.contains(.shift) ? .backTab : .tab
|
||||
case .kSpace: return nil
|
||||
case .kSymbolMenuPhysicalKeyIntl: return nil
|
||||
case .kBackSpace: return .backspace
|
||||
case .kEscape: return nil
|
||||
case .kCommand: return nil
|
||||
case .kShift: return nil
|
||||
case .kCapsLock: return nil
|
||||
case .kOption: return nil
|
||||
case .kControl: return nil
|
||||
case .kRightShift: return nil
|
||||
case .kRightOption: return nil
|
||||
case .kRightControl: return nil
|
||||
case .kFunction: return nil
|
||||
case .kF17: return .f17
|
||||
case .kVolumeUp: return nil
|
||||
case .kVolumeDown: return nil
|
||||
case .kMute: return nil
|
||||
case .kLineFeed: return nil // TODO: return 待釐清
|
||||
case .kF18: return .f18
|
||||
case .kF19: return .f19
|
||||
case .kF20: return .f20
|
||||
case .kYen: return nil
|
||||
case .kSymbolMenuPhysicalKeyJIS: return nil
|
||||
case .kJISNumPadComma: return nil
|
||||
case .kF5: return .f5
|
||||
case .kF6: return .f6
|
||||
case .kF7: return .f7
|
||||
case .kF3: return .f7
|
||||
case .kF8: return .f8
|
||||
case .kF9: return .f9
|
||||
case .kJISAlphanumericalKey: return nil
|
||||
case .kF11: return .f11
|
||||
case .kJISKanaSwappingKey: return nil
|
||||
case .kF13: return .f13
|
||||
case .kF16: return .f16
|
||||
case .kF14: return .f14
|
||||
case .kF10: return .f10
|
||||
case .kContextMenu: return .menu
|
||||
case .kF12: return .f12
|
||||
case .kF15: return .f15
|
||||
case .kHelp: return .help
|
||||
case .kHome: return .home
|
||||
case .kPageUp: return .pageUp
|
||||
case .kWindowsDelete: return .deleteForward
|
||||
case .kF4: return .f4
|
||||
case .kEnd: return .end
|
||||
case .kF2: return .f2
|
||||
case .kPageDown: return .pageDown
|
||||
case .kF1: return .f1
|
||||
case .kLeftArrow: return .leftArrow
|
||||
case .kRightArrow: return .rightArrow
|
||||
case .kDownArrow: return .downArrow
|
||||
case .kUpArrow: return .upArrow
|
||||
}
|
||||
}
|
||||
return rawData?.unicodeScalar
|
||||
}
|
||||
}
|
||||
|
||||
enum KeyCodeBlackListed: UInt16 {
|
||||
|
@ -506,6 +313,17 @@ let mapMainAreaNumKey: [UInt16: String] = [
|
|||
/// 注意:第 95 號 Key Code(逗號)為 JIS 佈局特有的數字小鍵盤按鍵。
|
||||
let arrNumpadKeyCodes: [UInt16] = [65, 67, 69, 71, 75, 78, 81, 82, 83, 84, 85, 86, 87, 88, 89, 91, 92, 95]
|
||||
|
||||
// CharCodes: https://theasciicode.com.ar/ascii-control-characters/horizontal-tab-ascii-code-9.html
|
||||
enum CharCode: UInt16 {
|
||||
case yajuusenpaiA = 114
|
||||
case yajuusenpaiB = 514
|
||||
case yajuusenpaiC = 1919
|
||||
case yajuusenpaiD = 810
|
||||
// CharCode is not reliable at all. KeyCode is the most appropriate choice due to its accuracy.
|
||||
// KeyCode doesn't give a phuque about the character sent through macOS keyboard layouts ...
|
||||
// ... but only focuses on which physical key is pressed.
|
||||
}
|
||||
|
||||
// MARK: - Emacs CharCode-KeyCode translation tables.
|
||||
|
||||
public enum EmacsKey {
|
||||
|
@ -515,17 +333,17 @@ public enum EmacsKey {
|
|||
|
||||
// MARK: - Apple ABC Keyboard Mapping
|
||||
|
||||
public extension KBEvent {
|
||||
func layoutTranslated(to layout: LatinKeyboardMappings = .qwerty) -> KBEvent {
|
||||
public extension NSEvent {
|
||||
func layoutTranslated(to layout: LatinKeyboardMappings = .qwerty) -> NSEvent {
|
||||
let mapTable = layout.mapTable
|
||||
if isFlagChanged { return self }
|
||||
guard keyModifierFlags == .shift || keyModifierFlags.isEmpty else { return self }
|
||||
if !mapTable.keys.contains(keyCode) { return self }
|
||||
guard let dataTuplet = mapTable[keyCode] else { return self }
|
||||
let result: KBEvent = reinitiate(
|
||||
let result: NSEvent? = reinitiate(
|
||||
characters: isShiftHold ? dataTuplet.1 : dataTuplet.0,
|
||||
charactersIgnoringModifiers: dataTuplet.0
|
||||
)
|
||||
return result
|
||||
return result ?? self
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import AppKit
|
||||
import InputMethodKit
|
||||
|
||||
public extension NSWindowController {
|
||||
func orderFront() {
|
||||
|
@ -48,19 +49,12 @@ public extension NSWindowController {
|
|||
}
|
||||
|
||||
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 {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = title
|
||||
if let text = text { alert.informativeText = text }
|
||||
alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
|
||||
var result: NSApplication.ModalResponse = .alertFirstButtonReturn
|
||||
guard let self = self else { return alert.runModal() }
|
||||
alert.beginSheetModal(for: self) { theResponce in
|
||||
result = theResponce
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
// (c) 2023 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.
|
||||
|
||||
/// A Swift script to check whether a non-system process is abusing the SecureEventInput.
|
||||
|
||||
import AppKit
|
||||
import IOKit
|
||||
|
||||
public enum SecureEventInputSputnik {
|
||||
public static func getIORegListResults() -> String? {
|
||||
var resultDictionaryCF: Unmanaged<CFMutableDictionary>?
|
||||
/// Regarding the parameter in IORegistryGetRootEntry:
|
||||
/// Both kIOMasterPortDefault and kIOMainPortDefault are 0.
|
||||
/// The latter one is similar to what `git` had done: changing "Master" to "Main".
|
||||
let statusSucceeded = IORegistryEntryCreateCFProperties(
|
||||
IORegistryGetRootEntry(0), &resultDictionaryCF, kCFAllocatorDefault, IOOptionBits(0)
|
||||
)
|
||||
guard statusSucceeded == KERN_SUCCESS else { return nil }
|
||||
let dict = resultDictionaryCF?.takeRetainedValue()
|
||||
guard let dict: [CFString: Any] = dict as? [CFString: Any] else { return nil }
|
||||
return (dict.description)
|
||||
}
|
||||
|
||||
/// Find all non-system processes using the SecureEventInput.
|
||||
/// - Parameter abusersOnly: List only non-frontmost processes.
|
||||
/// - **Reason to Use**: Non-frontmost processes of such are considered abusers of SecureEventInput,
|
||||
/// hindering 3rd-party input methods from being switched to by the user.
|
||||
/// They are also hindering users from accessing the menu of all 3rd-party input methods.
|
||||
/// There are Apple's internal business reasons why macOS always has lack of certain crucial input methods,
|
||||
/// plus that some some IMEs in macOS have certain bugs / defects for decades and are unlikely to be solved,
|
||||
/// making the sense that why there are needs of 3rd-party input methods.
|
||||
/// - **How to Use**: For example, one can use an NSTimer to run this function
|
||||
/// with `abusersOnly: true` every 15~60 seconds. Once the result dictionary is not empty,
|
||||
/// you may either warn the users to restart the matched process or directly terminate it.
|
||||
/// Note that you cannot terminate a process if your app is Sandboxed.
|
||||
/// - Returns: Matched results as a dictionary in `[Int32: NSRunningApplication]` format. The keys are PIDs.
|
||||
/// - Remark: The`"com.apple.SecurityAgent"` won't be included in the result since it is a system process.
|
||||
public static func getRunningSecureInputApps(abusersOnly: Bool = false) -> [Int32: NSRunningApplication] {
|
||||
var result = [Int32: NSRunningApplication]()
|
||||
guard let rawData = getIORegListResults() else { return result }
|
||||
rawData.enumerateLines { currentLine, _ in
|
||||
guard currentLine.contains("kCGSSessionSecureInputPID") else { return }
|
||||
guard let filteredNumStr = Int32(currentLine.filter("0123456789".contains)) else { return }
|
||||
guard let matchedApp = NSRunningApplication(processIdentifier: filteredNumStr) else { return }
|
||||
guard matchedApp.bundleIdentifier != "com.apple.SecurityAgent" else { return }
|
||||
if abusersOnly {
|
||||
guard !matchedApp.isActive else { return }
|
||||
}
|
||||
result[filteredNumStr] = matchedApp
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
// swift-tools-version:5.3
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
*/
|
||||
|
||||
import Foundation
|
||||
import SQLite3
|
||||
|
||||
#if os(Linux)
|
||||
import Glibc
|
||||
|
@ -32,56 +31,18 @@ import SQLite3
|
|||
import Darwin
|
||||
#endif
|
||||
|
||||
public enum DictType: Int, CaseIterable {
|
||||
case zhHantTW = 0
|
||||
case zhHantHK = 1
|
||||
case zhHansSG = 2
|
||||
case zhHansJP = 3
|
||||
case zhHantKX = 4
|
||||
case zhHansCN = 5
|
||||
|
||||
public static func match(rawKeyString: String) -> DictType? {
|
||||
DictType.allCases.filter { $0.rawKeyString == rawKeyString }.first
|
||||
}
|
||||
|
||||
public var rawKeyString: String {
|
||||
switch self {
|
||||
case .zhHantTW:
|
||||
return "zh2TW"
|
||||
case .zhHantHK:
|
||||
return "zh2HK"
|
||||
case .zhHansSG:
|
||||
return "zh2SG"
|
||||
case .zhHansJP:
|
||||
return "zh2JP"
|
||||
case .zhHantKX:
|
||||
return "zh2KX"
|
||||
case .zhHansCN:
|
||||
return "zh2CN"
|
||||
}
|
||||
}
|
||||
public enum DictType {
|
||||
case zhHantTW
|
||||
case zhHantHK
|
||||
case zhHansSG
|
||||
case zhHansJP
|
||||
case zhHantKX
|
||||
case zhHansCN
|
||||
}
|
||||
|
||||
public class HotenkaChineseConverter {
|
||||
private(set) var dict: [String: [String: String]]
|
||||
private var dictFiles: [String: [String]]
|
||||
var ptrSQL: OpaquePointer?
|
||||
|
||||
deinit {
|
||||
sqlite3_close_v2(ptrSQL)
|
||||
ptrSQL = nil
|
||||
}
|
||||
|
||||
public init(sqliteDir dbPath: String) {
|
||||
dict = .init()
|
||||
dictFiles = .init()
|
||||
guard sqlite3_open(dbPath, &ptrSQL) == SQLITE_OK else {
|
||||
NSLog("// Exception happened when connecting to SQLite database at: \(dbPath).")
|
||||
ptrSQL = nil
|
||||
return
|
||||
}
|
||||
sqlite3_exec(ptrSQL, "PRAGMA journal_mode = OFF;", nil, nil, nil)
|
||||
}
|
||||
|
||||
public init(plistDir: String) {
|
||||
dictFiles = .init()
|
||||
|
@ -100,7 +61,9 @@ public class HotenkaChineseConverter {
|
|||
dictFiles = .init()
|
||||
do {
|
||||
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
|
||||
} catch {
|
||||
NSLog("// Exception happened when reading dict json at: \(jsonDir).")
|
||||
|
@ -174,29 +137,27 @@ public class HotenkaChineseConverter {
|
|||
|
||||
// MARK: - Public Methods
|
||||
|
||||
public func query(dict dictType: DictType, key searchKey: String) -> String? {
|
||||
guard ptrSQL != nil else { return dict[dictType.rawKeyString]?[searchKey] }
|
||||
var ptrStatement: OpaquePointer?
|
||||
let sqlQuery = "SELECT * FROM DATA_HOTENKA WHERE dict=\(dictType.rawValue) AND theKey='\(searchKey)';"
|
||||
sqlite3_prepare_v2(ptrSQL, sqlQuery, -1, &ptrStatement, nil)
|
||||
defer {
|
||||
sqlite3_finalize(ptrStatement)
|
||||
ptrStatement = nil
|
||||
}
|
||||
// 此處只需要用到第一筆結果。
|
||||
while sqlite3_step(ptrStatement) == SQLITE_ROW {
|
||||
guard let rawValue = sqlite3_column_text(ptrStatement, 2) else { continue }
|
||||
return String(cString: rawValue)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func convert(_ target: String, to dictType: DictType) -> String {
|
||||
var result = ""
|
||||
if ptrSQL == nil {
|
||||
guard dict[dictType.rawKeyString] != nil else { return target }
|
||||
var dictTypeKey: String
|
||||
|
||||
switch dictType {
|
||||
case .zhHantTW:
|
||||
dictTypeKey = "zh2TW"
|
||||
case .zhHantHK:
|
||||
dictTypeKey = "zh2HK"
|
||||
case .zhHansSG:
|
||||
dictTypeKey = "zh2SG"
|
||||
case .zhHansJP:
|
||||
dictTypeKey = "zh2JP"
|
||||
case .zhHantKX:
|
||||
dictTypeKey = "zh2KX"
|
||||
case .zhHansCN:
|
||||
dictTypeKey = "zh2CN"
|
||||
}
|
||||
|
||||
var result = ""
|
||||
guard let useDict = dict[dictTypeKey] else { return target }
|
||||
|
||||
var i = 0
|
||||
while i < (target.count) {
|
||||
let max = (target.count) - i
|
||||
|
@ -206,7 +167,7 @@ public class HotenkaChineseConverter {
|
|||
innerloop: while j > 0 {
|
||||
let start = target.index(target.startIndex, offsetBy: i)
|
||||
let end = target.index(target.startIndex, offsetBy: i + j)
|
||||
guard let useDictSubStr = query(dict: dictType, key: String(target[start ..< end])) else {
|
||||
guard let useDictSubStr = useDict[String(target[start ..< end])] else {
|
||||
j -= 1
|
||||
continue
|
||||
}
|
||||
|
|
|
@ -40,10 +40,7 @@ extension HotenkaTests {
|
|||
let testInstance: HotenkaChineseConverter = .init(dictDir: testDataPath)
|
||||
NSLog("// Loading complete. Generating json dict file.")
|
||||
do {
|
||||
let urlOutput = URL(fileURLWithPath: testDataPath + "convdict.json")
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .sortedKeys
|
||||
try encoder.encode(testInstance.dict).write(to: urlOutput, options: .atomic)
|
||||
try JSONSerialization.data(withJSONObject: testInstance.dict, options: .sortedKeys).write(to: URL(fileURLWithPath: testDataPath + "convdict.json"))
|
||||
} catch {
|
||||
NSLog("// Error on writing strings to file: \(error)")
|
||||
}
|
||||
|
|
|
@ -1,113 +0,0 @@
|
|||
// Swiftified by (c) 2022 and onwards The vChewing Project (MIT-NTL License).
|
||||
// Rebranded from (c) Nick Chen's Obj-C library "NCChineseConverter" (MIT License).
|
||||
/*
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
1. The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
2. 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 above.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import SQLite3
|
||||
import XCTest
|
||||
|
||||
@testable import Hotenka
|
||||
|
||||
private let packageRootPath = URL(fileURLWithPath: #file).pathComponents.prefix(while: { $0 != "Tests" }).joined(
|
||||
separator: "/"
|
||||
).dropFirst()
|
||||
|
||||
private let testDataPath: String = packageRootPath + "/Tests/TestDictData/"
|
||||
|
||||
extension HotenkaTests {
|
||||
func testGeneratingSQLiteDB() throws {
|
||||
NSLog("// Start loading from: \(packageRootPath)")
|
||||
let testInstance: HotenkaChineseConverter = .init(dictDir: testDataPath)
|
||||
NSLog("// Loading complete. Generating SQLite database.")
|
||||
var ptrSQL: OpaquePointer?
|
||||
let dbPath = testDataPath + "convdict.sqlite"
|
||||
|
||||
XCTAssertTrue(
|
||||
sqlite3_open(dbPath, &ptrSQL) == SQLITE_OK,
|
||||
"HOTENKA: SQLite Database Initialization Error."
|
||||
)
|
||||
XCTAssertTrue(
|
||||
sqlite3_exec(ptrSQL, "PRAGMA synchronous = OFF;", nil, nil, nil) == SQLITE_OK,
|
||||
"HOTENKA: SQLite synchronous OFF failed."
|
||||
)
|
||||
XCTAssertTrue(
|
||||
sqlite3_exec(ptrSQL, "PRAGMA journal_mode = OFF;", nil, nil, nil) == SQLITE_OK,
|
||||
"HOTENKA: SQLite journal_mode OFF failed."
|
||||
)
|
||||
|
||||
let sqlMakeTableHotenka = """
|
||||
DROP TABLE IF EXISTS DATA_HOTENKA;
|
||||
CREATE TABLE IF NOT EXISTS DATA_HOTENKA (
|
||||
dict INTEGER,
|
||||
theKey TEXT,
|
||||
theValue TEXT,
|
||||
PRIMARY KEY (dict, theKey)
|
||||
) WITHOUT ROWID;
|
||||
"""
|
||||
|
||||
XCTAssertTrue(
|
||||
sqlite3_exec(ptrSQL, sqlMakeTableHotenka, nil, nil, nil) == SQLITE_OK,
|
||||
"HOTENKA: SQLite Table Creation Failed."
|
||||
)
|
||||
|
||||
assert(sqlite3_exec(ptrSQL, "begin;", nil, nil, nil) == SQLITE_OK)
|
||||
|
||||
testInstance.dict.forEach { dictName, subDict in
|
||||
guard let dictID = DictType.match(rawKeyString: dictName)?.rawValue else { return }
|
||||
subDict.forEach { key, value in
|
||||
var ptrStatement: OpaquePointer?
|
||||
let sqlInsertion = "INSERT INTO DATA_HOTENKA (dict, theKey, theValue) VALUES (\(dictID), '\(key)', '\(value)')"
|
||||
assert(
|
||||
sqlite3_prepare_v2(
|
||||
ptrSQL, sqlInsertion, -1, &ptrStatement, nil
|
||||
) == SQLITE_OK,
|
||||
"HOTENKA: Failed from preparing: \(sqlInsertion)"
|
||||
)
|
||||
assert(
|
||||
sqlite3_step(ptrStatement) == SQLITE_DONE,
|
||||
"HOTENKA: Failed from stepping: \(sqlInsertion)"
|
||||
)
|
||||
sqlite3_finalize(ptrStatement)
|
||||
ptrStatement = nil
|
||||
}
|
||||
}
|
||||
assert(sqlite3_exec(ptrSQL, "commit;", nil, nil, nil) == SQLITE_OK)
|
||||
sqlite3_close_v2(ptrSQL)
|
||||
}
|
||||
|
||||
func testSampleWithSQLiteDB() throws {
|
||||
NSLog("// Start loading plist from: \(packageRootPath)")
|
||||
let testInstance2: HotenkaChineseConverter = .init(sqliteDir: testDataPath + "convdict.sqlite")
|
||||
NSLog("// Successfully loading sql dictionary.")
|
||||
|
||||
let oriString = "为中华崛起而读书"
|
||||
let result1 = testInstance2.convert(oriString, to: .zhHantTW)
|
||||
let result2 = testInstance2.convert(result1, to: .zhHantKX)
|
||||
let result3 = testInstance2.convert(result2, to: .zhHansJP)
|
||||
NSLog("// Results: \(result1) \(result2) \(result3)")
|
||||
XCTAssertEqual(result1, "為中華崛起而讀書")
|
||||
XCTAssertEqual(result2, "爲中華崛起而讀書")
|
||||
XCTAssertEqual(result3, "為中華崛起而読書")
|
||||
}
|
||||
}
|
|
@ -15,13 +15,23 @@ public enum IMKHelper {
|
|||
/// 威注音有專門統計過,實際上會有差異的英數鍵盤佈局只有這幾種。
|
||||
/// 精簡成這種清單的話,不但節省 SwiftUI 的繪製壓力,也方便使用者做選擇。
|
||||
public static let arrWhitelistedKeyLayoutsASCII: [String] = {
|
||||
var results = LatinKeyboardMappings.allCases
|
||||
if #available(macOS 10.13, *) {
|
||||
results = results.filter {
|
||||
![.qwertyUS, .qwertzGerman, .azertyFrench].contains($0)
|
||||
}
|
||||
var result = [
|
||||
"com.apple.keylayout.ABC",
|
||||
"com.apple.keylayout.ABC-AZERTY",
|
||||
"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 results.map(\.rawValue)
|
||||
return result
|
||||
}()
|
||||
|
||||
public static let arrDynamicBasicKeyLayouts: [String] = [
|
||||
|
@ -39,29 +49,31 @@ public enum IMKHelper {
|
|||
"org.unknown.keylayout.vChewingMiTAC",
|
||||
]
|
||||
|
||||
public static var allowedAlphanumericalTISInputSources: [TISInputSource.KeyboardLayout] {
|
||||
let allTISKeyboardLayouts = TISInputSource.getAllTISInputKeyboardLayoutMap()
|
||||
return arrWhitelistedKeyLayoutsASCII.compactMap { allTISKeyboardLayouts[$0] }
|
||||
public static var allowedAlphanumericalTISInputSources: [TISInputSource] {
|
||||
arrWhitelistedKeyLayoutsASCII.compactMap { TISInputSource.generate(from: $0) }
|
||||
}
|
||||
|
||||
public static var allowedBasicLayoutsAsTISInputSources: [TISInputSource.KeyboardLayout?] {
|
||||
let allTISKeyboardLayouts = TISInputSource.getAllTISInputKeyboardLayoutMap()
|
||||
// 為了保證清單順序,先弄幾個容器。
|
||||
var containerA: [TISInputSource.KeyboardLayout?] = []
|
||||
var containerB: [TISInputSource.KeyboardLayout?] = []
|
||||
var containerC: [TISInputSource.KeyboardLayout] = []
|
||||
public static var allowedBasicLayoutsAsTISInputSources: [TISInputSource?] {
|
||||
// 為了保證清單順序,先弄兩個容器。
|
||||
var containerA: [TISInputSource?] = []
|
||||
var containerB: [TISInputSource?] = []
|
||||
var containerC: [TISInputSource?] = []
|
||||
|
||||
let filterSet = Array(Set(arrWhitelistedKeyLayoutsASCII).subtracting(Set(arrDynamicBasicKeyLayouts)))
|
||||
let matchedGroupBasic = (arrWhitelistedKeyLayoutsASCII + arrDynamicBasicKeyLayouts).compactMap {
|
||||
allTISKeyboardLayouts[$0]
|
||||
}
|
||||
matchedGroupBasic.forEach { neta in
|
||||
if filterSet.contains(neta.id) {
|
||||
let rawDictionary = TISInputSource.rawTISInputSources(onlyASCII: false)
|
||||
|
||||
Self.arrWhitelistedKeyLayoutsASCII.forEach {
|
||||
if let neta = rawDictionary[$0], !arrDynamicBasicKeyLayouts.contains(neta.identifier) {
|
||||
containerC.append(neta)
|
||||
} else if neta.id.hasPrefix("com.apple") {
|
||||
containerA.append(neta)
|
||||
} else {
|
||||
containerB.append(neta)
|
||||
}
|
||||
}
|
||||
|
||||
Self.arrDynamicBasicKeyLayouts.forEach {
|
||||
if let neta = rawDictionary[$0] {
|
||||
if neta.identifier.contains("com.apple") {
|
||||
containerA.append(neta)
|
||||
} else {
|
||||
containerB.append(neta)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,27 +8,25 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public enum LatinKeyboardMappings: String, CaseIterable {
|
||||
public enum LatinKeyboardMappings: String {
|
||||
case qwerty = "com.apple.keylayout.ABC"
|
||||
case qwertyBritish = "com.apple.keylayout.British"
|
||||
case qwertyUS = "com.apple.keylayout.US" // 10.9 - 10.12
|
||||
case qwertyUS = "com.apple.keylayout.US"
|
||||
case azerty = "com.apple.keylayout.ABC-AZERTY"
|
||||
case azertyFrench = "com.apple.keylayout.French"
|
||||
case qwertz = "com.apple.keylayout.ABC-QWERTZ"
|
||||
case azertyFrench = "com.apple.keylayout.French" // 10.9 - 10.12
|
||||
case qwertzGerman = "com.apple.keylayout.German" // 10.9 - 10.12
|
||||
case qwertyGerman = "com.apple.keylayout.German"
|
||||
case colemak = "com.apple.keylayout.Colemak"
|
||||
case dvorak = "com.apple.keylayout.Dvorak"
|
||||
case dvorakQwertyCMD = "com.apple.keylayout.DVORAK-QWERTYCMD"
|
||||
case dvorakLeft = "com.apple.keylayout.Dvorak-Left"
|
||||
case dvorakRight = "com.apple.keylayout.Dvorak-Right"
|
||||
|
||||
public var mapTable: [UInt16: (String, String)] {
|
||||
switch self {
|
||||
case .qwerty, .qwertyUS, .qwertyBritish: return Self.dictQwerty
|
||||
case .qwerty, .qwertyUS: return Self.dictQwerty
|
||||
case .azerty, .azertyFrench: return Self.dictAzerty
|
||||
case .qwertz, .qwertzGerman: return Self.dictQwertz
|
||||
case .qwertz, .qwertyGerman: return Self.dictQwertz
|
||||
case .colemak: return Self.dictColemak
|
||||
case .dvorak, .dvorakQwertyCMD: return Self.dictDvorak
|
||||
case .dvorak: return Self.dictDvorak
|
||||
case .dvorakLeft: return Self.dictDvorakLeft
|
||||
case .dvorakRight: return Self.dictDvorakRight
|
||||
}
|
||||
|
|
|
@ -12,13 +12,8 @@ import InputMethodKit
|
|||
// MARK: - TISInputSource Extension by The vChewing Project (MIT-NTL License).
|
||||
|
||||
public extension TISInputSource {
|
||||
struct KeyboardLayout: Identifiable {
|
||||
public var id: String
|
||||
public var titleLocalized: String
|
||||
}
|
||||
|
||||
static var allRegisteredInstancesOfThisInputMethod: [TISInputSource] {
|
||||
TISInputSource.match(modeIDs: TISInputSource.modes)
|
||||
TISInputSource.modes.compactMap { TISInputSource.generate(from: $0) }
|
||||
}
|
||||
|
||||
static var modes: [String] {
|
||||
|
@ -27,7 +22,7 @@ public extension TISInputSource {
|
|||
else {
|
||||
return []
|
||||
}
|
||||
return tsInputModeListKey.keys.map(\.description)
|
||||
return tsInputModeListKey.keys.map { $0 }
|
||||
}
|
||||
|
||||
@discardableResult static func registerInputMethod() -> Bool {
|
||||
|
@ -85,6 +80,10 @@ public extension TISInputSource {
|
|||
== kCFBooleanTrue
|
||||
}
|
||||
|
||||
static func generate(from identifier: String) -> TISInputSource? {
|
||||
TISInputSource.rawTISInputSources(onlyASCII: false)[identifier]
|
||||
}
|
||||
|
||||
var inputModeID: 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
|
||||
}
|
||||
|
||||
// Refactored by Shiki Suen.
|
||||
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] {
|
||||
static func rawTISInputSources(onlyASCII: Bool = false) -> [String: TISInputSource] {
|
||||
// 為了指定檢索條件,先構築 CFDictionary 辭典。
|
||||
// 第二項代指辭典容量。
|
||||
let dicConditions: [CFString: Any] = !onlyASCII ? [:] : [
|
||||
kTISPropertyInputSourceType: kTISTypeKeyboardLayout as CFString,
|
||||
kTISPropertyInputSourceIsASCIICapable: kCFBooleanTrue as CFBoolean,
|
||||
|
@ -158,21 +132,10 @@ public extension TISInputSource {
|
|||
if onlyASCII {
|
||||
result = result.filter { $0.scriptCode == 0 }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/// 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] = [:]
|
||||
var resultDictionary: [String: TISInputSource] = [:]
|
||||
result.forEach {
|
||||
let newNeta1 = TISInputSource.KeyboardLayout(id: $0.inputModeID, titleLocalized: $0.vChewingLocalizedName)
|
||||
let newNeta2 = TISInputSource.KeyboardLayout(id: $0.identifier, titleLocalized: $0.vChewingLocalizedName)
|
||||
resultDictionary[$0.inputModeID] = newNeta1
|
||||
resultDictionary[$0.identifier] = newNeta2
|
||||
resultDictionary[$0.inputModeID] = $0
|
||||
resultDictionary[$0.identifier] = $0
|
||||
}
|
||||
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: [
|
||||
.package(path: "../RMJay_LineReader"),
|
||||
.package(path: "../vChewing_Megrez"),
|
||||
.package(path: "../vChewing_SwiftExtension"),
|
||||
.package(path: "../vChewing_PinyinPhonaConverter"),
|
||||
.package(path: "../vChewing_Shared"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
|
@ -23,7 +24,8 @@ let package = Package(
|
|||
dependencies: [
|
||||
.product(name: "LineReader", package: "RMJay_LineReader"),
|
||||
.product(name: "Megrez", package: "vChewing_Megrez"),
|
||||
.product(name: "SwiftExtension", package: "vChewing_SwiftExtension"),
|
||||
.product(name: "Shared", package: "vChewing_Shared"),
|
||||
.product(name: "PinyinPhonaConverter", package: "vChewing_PinyinPhonaConverter"),
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
# LangModelAssembly
|
||||
|
||||
威注音輸入法的語言模組總成套裝,以 LMAssembly 命名空間承載下述唯二對外物件:
|
||||
威注音輸入法的語言模組總成套裝。
|
||||
|
||||
- vChewingLM:總命名空間,也承載一些在套裝內共用的工具函式。
|
||||
- LMConsolidator:自動格式整理模組。
|
||||
- LMInstantiator:語言模組副本化模組,亦集成一些自身功能擴展。
|
||||
|
||||
LMAssembly 總命名空間也承載一些在套裝內共用的工具函式。
|
||||
- LMInstantiator:語言模組副本化模組。另有其日期時間擴充模組可用(對 CIN 磁帶模式無效)。
|
||||
|
||||
以下是子模組:
|
||||
|
||||
- LMAssociates:關聯詞語模組。
|
||||
- lmCassette:專門用來處理 CIN 磁帶檔案的模組,命名為「遠野」引擎。
|
||||
- LMAssociates:聯想詞模組。
|
||||
- LMCoreEX:可以直接讀取 TXT 格式的帶有權重資料的語彙檔案的模組。
|
||||
- LMCoreJSON:專門用來讀取原廠 JSON 檔案的模組。
|
||||
- lmPlainBopomofo:專門用來讀取使用者自訂ㄅ半候選字順序覆蓋定義檔案(plist)的模組。
|
||||
- lmReplacements:專門用來讀取使用者語彙置換模式的辭典資料的模組。
|
||||
- lmUserOverride:半衰記憶模組。
|
||||
|
|
|
@ -1,249 +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
|
||||
|
||||
/// 工作原理:先用 InputToken.parse 分析原始字串,給出準確的 Token。
|
||||
/// 然後再讓這個 Token 用 .translated() 自我表述出轉換結果。
|
||||
|
||||
extension LMAssembly {
|
||||
enum InputToken {
|
||||
case timeZone(shortened: Bool)
|
||||
case timeNow(shortened: Bool)
|
||||
case date(dayDelta: Int = 0, yearDelta: Int = 0, shortened: Bool = true, luna: Bool = false)
|
||||
case week(dayDelta: Int = 0, shortened: Bool = true)
|
||||
case year(yearDelta: Int = 0)
|
||||
case yearGanzhi(yearDelta: Int = 0)
|
||||
case yearZodiac(yearDelta: Int = 0)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 正式對外投入使用的 API。
|
||||
|
||||
public extension String {
|
||||
func parseAsInputToken(isCHS: Bool) -> [String] {
|
||||
LMAssembly.InputToken.parse(from: self).map { $0.translated(isCHS: isCHS) }.flatMap { $0 }.deduplicated
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Parser parsing raw token value to construct token.
|
||||
|
||||
extension LMAssembly.InputToken {
|
||||
static func parse(from rawToken: String) -> [LMAssembly.InputToken] {
|
||||
var result: [LMAssembly.InputToken] = []
|
||||
guard rawToken.prefix(6) == "MACRO@" else { return result }
|
||||
var mapParams: [String: Int] = [:]
|
||||
let tokenComponents = rawToken.dropFirst(6).split(separator: "_").map { param in
|
||||
let result = param.uppercased()
|
||||
let kvPair = param.split(separator: ":")
|
||||
guard kvPair.count == 2 else { return result }
|
||||
guard let pairValue = Int(kvPair[1]) else { return result }
|
||||
mapParams[kvPair[0].description] = pairValue
|
||||
return result
|
||||
}
|
||||
guard !tokenComponents.isEmpty else { return result }
|
||||
// 準備接收參數。
|
||||
let dayDelta: Int = mapParams["dayDelta".uppercased()] ?? 0
|
||||
let yearDelta: Int = mapParams["yearDelta".uppercased()] ?? 0
|
||||
let shortened: Bool = tokenComponents.contains("SHORTENED")
|
||||
let hasZodiac: Bool = tokenComponents.contains("ZODIAC")
|
||||
let hasGanzhi: Bool = tokenComponents.contains("GANZHI")
|
||||
let hasLuna: Bool = tokenComponents.contains("LUNA")
|
||||
|
||||
switch tokenComponents[0] {
|
||||
case "TIMEZONE": result.append(.timeZone(shortened: shortened))
|
||||
case "TIME": result.append(.timeNow(shortened: shortened))
|
||||
case "DATE": result.append(.date(dayDelta: dayDelta, yearDelta: yearDelta, shortened: shortened, luna: hasLuna))
|
||||
case "WEEK": result.append(.week(dayDelta: dayDelta, shortened: shortened))
|
||||
case "YEAR": result.append(.year(yearDelta: yearDelta)) // 始終插入公曆年,方便對比參考。
|
||||
if hasZodiac { result.append(.yearZodiac(yearDelta: yearDelta)) }
|
||||
if hasGanzhi { result.append(.yearGanzhi(yearDelta: yearDelta)) }
|
||||
default: break
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Parser parsing token itself.
|
||||
|
||||
extension LMAssembly.InputToken {
|
||||
func translated(isCHS: Bool) -> [String] {
|
||||
let locale = Locale(identifier: isCHS ? "zh-Hans" : "zh-Hant-TW")
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = locale
|
||||
let currentDate = Date()
|
||||
var dateToDescribe = currentDate // 接下來會針對給定參數修正這個資料值。
|
||||
var results: [String] = []
|
||||
|
||||
/// 內部函式,用來修正 dateToDescribe 自身的參數值。
|
||||
func applyDelta(for type: Calendar.Component, delta deltaValue: Int) {
|
||||
switch type {
|
||||
case .year:
|
||||
var delta = DateComponents()
|
||||
let thisYear = Calendar.current.dateComponents([.year], from: currentDate).year ?? 2018
|
||||
delta.year = max(deltaValue, thisYear * -1)
|
||||
dateToDescribe = Calendar.current.date(byAdding: delta, to: currentDate) ?? currentDate
|
||||
case .day:
|
||||
let dayLength = 60 * 60 * 24
|
||||
dateToDescribe = dateToDescribe.addingTimeInterval(Double(dayLength * deltaValue))
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
// 計算結果。
|
||||
switch self {
|
||||
case let .timeZone(shortened): // 時區
|
||||
let resultToAdd = TimeZone.current.localizedName(
|
||||
for: shortened ? .shortGeneric : .standard, locale: locale
|
||||
) ?? TimeZone.current.description
|
||||
results.append(resultToAdd)
|
||||
case let .timeNow(shortened): // 當前時間
|
||||
var formats = [String]()
|
||||
switch (isCHS, shortened) {
|
||||
case (false, true): formats.append(contentsOf: ["HH:mm", "HH點mm分", "HH時mm分"])
|
||||
case (false, false): formats.append(contentsOf: ["HH:mm:ss", "HH點mm分ss秒", "HH時mm分ss秒"])
|
||||
case (true, true): formats.append(contentsOf: ["HH:mm", "HH点mm分", "HH时mm分"])
|
||||
case (true, false): formats.append(contentsOf: ["HH:mm:ss", "HH点mm分ss秒", "HH时mm分ss秒"])
|
||||
}
|
||||
formats.forEach { formatString in
|
||||
formatter.dateFormat = formatString
|
||||
results.append(formatter.string(from: dateToDescribe))
|
||||
}
|
||||
let resultsExtra: [String] = results.compactMap {
|
||||
guard !$0.contains(":") else { return nil }
|
||||
var newResult = $0
|
||||
if newResult.first == "0" { newResult = newResult.dropFirst().description }
|
||||
if newResult.prefix(2) == "2点" || newResult.prefix(2) == "2點" {
|
||||
newResult = (isCHS ? "两点" : "兩點") + newResult.dropFirst(2).description
|
||||
}
|
||||
newResult = newResult.convertArabicNumeralsToChinese(onlyDigits: false)
|
||||
newResult = newResult.replacingOccurrences(of: "〇", with: "零")
|
||||
return newResult
|
||||
}
|
||||
results.append(contentsOf: resultsExtra)
|
||||
case let .date(dayDelta, yearDelta, shortened, hasLuna): // 日期
|
||||
applyDelta(for: .year, delta: yearDelta)
|
||||
applyDelta(for: .day, delta: dayDelta)
|
||||
// 農曆單獨處理。
|
||||
guard !hasLuna else {
|
||||
formatter.calendar = .init(identifier: .chinese)
|
||||
formatter.dateStyle = .medium
|
||||
formatter.dateFormat = "MMMd"
|
||||
let dateString = formatter.string(from: dateToDescribe)
|
||||
formatter.dateFormat = "U"
|
||||
let yearGanzhi = formatter.string(from: dateToDescribe)
|
||||
results.append("\(yearGanzhi)年\(dateString)")
|
||||
if let yearZodiac = mapGanzhiToZodiac[yearGanzhi] {
|
||||
results.append("\(isCHS ? yearZodiac.1 : yearZodiac.0)年\(dateString)")
|
||||
}
|
||||
break
|
||||
}
|
||||
let formats: [String] = [
|
||||
"MM-dd", "M月d日", "MM月dd日",
|
||||
]
|
||||
var additionalResult: String?
|
||||
for (i, formatString) in formats.enumerated() {
|
||||
formatter.dateFormat = formatString
|
||||
let dateStr = formatter.string(from: dateToDescribe)
|
||||
switch (i == 0, shortened) {
|
||||
case (false, true): formatter.dateFormat = "yy年"
|
||||
case (true, false): formatter.dateFormat = "y-"
|
||||
case (false, false): formatter.dateFormat = "y年"
|
||||
case (true, true): formatter.dateFormat = "yy-"
|
||||
}
|
||||
let yearStr = formatter.string(from: dateToDescribe)
|
||||
if i == 1 {
|
||||
let anotherDateStr = dateStr.convertArabicNumeralsToChinese(onlyDigits: false)
|
||||
let anotherYearStr = yearStr.convertArabicNumeralsToChinese(onlyDigits: true)
|
||||
additionalResult = anotherYearStr + anotherDateStr
|
||||
}
|
||||
let newResult = yearStr + dateStr
|
||||
guard !results.contains(newResult) else { continue }
|
||||
results.append(newResult)
|
||||
}
|
||||
if let additionalResult = additionalResult {
|
||||
results.append(additionalResult)
|
||||
}
|
||||
case let .week(dayDelta, shortened): // 星期
|
||||
applyDelta(for: .day, delta: dayDelta)
|
||||
formatter.dateFormat = shortened ? "EE" : "EEEE"
|
||||
results.append(formatter.string(from: dateToDescribe))
|
||||
case let .year(yearDelta): // 年度
|
||||
applyDelta(for: .year, delta: yearDelta)
|
||||
formatter.dateFormat = "U年"
|
||||
formatter.calendar = .init(identifier: .gregorian)
|
||||
let result = formatter.string(from: dateToDescribe)
|
||||
results.append(result)
|
||||
results.append(result.convertArabicNumeralsToChinese(onlyDigits: true))
|
||||
case let .yearGanzhi(yearDelta): // 幹支(其實嚴格來講「干支」才是錯的)
|
||||
applyDelta(for: .year, delta: yearDelta)
|
||||
formatter.dateFormat = "U年"
|
||||
formatter.calendar = .init(identifier: .chinese)
|
||||
let result = formatter.string(from: dateToDescribe)
|
||||
results.append(result)
|
||||
case let .yearZodiac(yearDelta): // 十二生肖
|
||||
applyDelta(for: .year, delta: yearDelta)
|
||||
formatter.dateFormat = "U"
|
||||
formatter.calendar = .init(identifier: .chinese)
|
||||
let rawKey = formatter.string(from: dateToDescribe)
|
||||
guard let rawResultPair = mapGanzhiToZodiac[rawKey] else { break }
|
||||
let rawResult = isCHS ? rawResultPair.1 : rawResultPair.0
|
||||
results.append(rawResult + "年")
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
}
|
||||
|
||||
/// 註一:天干地支在簡體中文與繁體中文的寫法完全雷同。
|
||||
/// 註二:此處採吐蕃的陰陽五行生肖法、而非突厥五行納音生肖法。
|
||||
private let mapGanzhiToZodiac: [String: (String, String)] = [
|
||||
"甲子": ("木鼠", "木鼠"), "乙丑": ("木牛", "木牛"), "丙寅": ("火虎", "火虎"), "丁卯": ("火兔", "火兔"),
|
||||
"戊辰": ("土龍", "土龙"), "己巳": ("土蛇", "土蛇"), "庚午": ("金馬", "金马"), "辛未": ("金羊", "金羊"),
|
||||
"壬申": ("水猴", "水猴"), "癸酉": ("水雞", "水鸡"), "甲戌": ("木狗", "木狗"), "乙亥": ("木豬", "木猪"),
|
||||
"丙子": ("火鼠", "火鼠"), "丁丑": ("火牛", "火牛"), "戊寅": ("土虎", "土虎"), "己卯": ("土兔", "土兔"),
|
||||
"庚辰": ("金龍", "金龙"), "辛巳": ("金蛇", "金蛇"), "壬午": ("水馬", "水马"), "癸未": ("水羊", "水羊"),
|
||||
"甲申": ("木猴", "木猴"), "乙酉": ("木雞", "木鸡"), "丙戌": ("火狗", "火狗"), "丁亥": ("火豬", "火猪"),
|
||||
"戊子": ("土鼠", "土鼠"), "己丑": ("土牛", "土牛"), "庚寅": ("金虎", "金虎"), "辛卯": ("金兔", "金兔"),
|
||||
"壬辰": ("水龍", "水龙"), "癸巳": ("水蛇", "水蛇"), "甲午": ("木馬", "木马"), "乙未": ("木羊", "木羊"),
|
||||
"丙申": ("火猴", "火猴"), "丁酉": ("火雞", "火鸡"), "戊戌": ("土狗", "土狗"), "己亥": ("土豬", "土猪"),
|
||||
"庚子": ("金鼠", "金鼠"), "辛丑": ("金牛", "金牛"), "壬寅": ("水虎", "水虎"), "癸卯": ("水兔", "水兔"),
|
||||
"甲辰": ("木龍", "木龙"), "乙巳": ("木蛇", "木蛇"), "丙午": ("火馬", "火马"), "丁未": ("火羊", "火羊"),
|
||||
"戊申": ("土猴", "土猴"), "己酉": ("土雞", "土鸡"), "庚戌": ("金狗", "金狗"), "辛亥": ("金豬", "金猪"),
|
||||
"壬子": ("水鼠", "水鼠"), "癸丑": ("水牛", "水牛"), "甲寅": ("木虎", "木虎"), "乙卯": ("木兔", "木兔"),
|
||||
"丙辰": ("火龍", "火龙"), "丁巳": ("火蛇", "火蛇"), "戊午": ("土馬", "土马"), "己未": ("土羊", "土羊"),
|
||||
"庚申": ("金猴", "金猴"), "辛酉": ("金雞", "金鸡"), "壬戌": ("水狗", "水狗"), "癸亥": ("水豬", "水猪"),
|
||||
]
|
||||
|
||||
// MARK: - Date Time Language Conversion Extension
|
||||
|
||||
private let tableMappingArabicDatesToChinese: [String: String] = {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.locale = Locale(identifier: "zh-Hant-TW") // 預設是英文,設定為中文。繁簡一致。
|
||||
formatter.numberStyle = .spellOut
|
||||
var result = [String: String]()
|
||||
for i in 0 ... 60 {
|
||||
result[i.description] = formatter.string(from: NSNumber(value: i))
|
||||
}
|
||||
return result
|
||||
}()
|
||||
|
||||
private extension String {
|
||||
/// 將給定的字串當中的阿拉伯數字轉為漢語小寫,逐字轉換。
|
||||
/// - Parameter target: 要進行轉換操作的對象,會直接修改該對象。
|
||||
func convertArabicNumeralsToChinese(onlyDigits: Bool) -> String {
|
||||
var target = self
|
||||
let sortedKeys = tableMappingArabicDatesToChinese.keys.sorted { $0.count > $1.count }
|
||||
for key in sortedKeys {
|
||||
if onlyDigits, key.count > 1 { continue }
|
||||
guard let result = tableMappingArabicDatesToChinese[key] else { continue }
|
||||
target = target.replacingOccurrences(of: key, with: result)
|
||||
}
|
||||
return target
|
||||
}
|
||||
}
|
|
@ -8,8 +8,9 @@
|
|||
|
||||
import Foundation
|
||||
import LineReader
|
||||
import Shared
|
||||
|
||||
public extension LMAssembly {
|
||||
public extension vChewingLM {
|
||||
enum LMConsolidator {
|
||||
public static let kPragmaHeader = "# 𝙵𝙾𝚁𝙼𝙰𝚃 𝚘𝚛𝚐.𝚊𝚝𝚎𝚕𝚒𝚎𝚛𝙸𝚗𝚖𝚞.𝚟𝚌𝚑𝚎𝚠𝚒𝚗𝚐.𝚞𝚜𝚎𝚛𝙻𝚊𝚗𝚐𝚞𝚊𝚐𝚎𝙼𝚘𝚍𝚎𝚕𝙳𝚊𝚝𝚊.𝚏𝚘𝚛𝚖𝚊𝚝𝚝𝚎𝚍"
|
||||
|
||||
|
@ -25,19 +26,19 @@ public extension LMAssembly {
|
|||
let lineReader = try LineReader(file: fileHandle)
|
||||
for strLine in lineReader { // 不需要 i=0,因為第一遍迴圈就出結果。
|
||||
if strLine != kPragmaHeader {
|
||||
vCLMLog("Header Mismatch, Starting In-Place Consolidation.")
|
||||
vCLog("Header Mismatch, Starting In-Place Consolidation.")
|
||||
return false
|
||||
} else {
|
||||
vCLMLog("Header Verification Succeeded: \(strLine).")
|
||||
vCLog("Header Verification Succeeded: \(strLine).")
|
||||
return true
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
vCLMLog("Header Verification Failed: File Access Error.")
|
||||
vCLog("Header Verification Failed: File Access Error.")
|
||||
return false
|
||||
}
|
||||
}
|
||||
vCLMLog("Header Verification Failed: File Missing.")
|
||||
vCLog("Header Verification Failed: File Missing.")
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -50,12 +51,12 @@ public extension LMAssembly {
|
|||
let dict = try FileManager.default.attributesOfItem(atPath: path)
|
||||
if let value = dict[FileAttributeKey.size] as? UInt64 { fileSize = value }
|
||||
} catch {
|
||||
vCLMLog("EOF Fix Failed: File Missing at \(path).")
|
||||
vCLog("EOF Fix Failed: File Missing at \(path).")
|
||||
return false
|
||||
}
|
||||
guard let fileSize = fileSize else { return false }
|
||||
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
|
||||
}
|
||||
defer { writeFile.closeFile() }
|
||||
|
@ -63,11 +64,11 @@ public extension LMAssembly {
|
|||
/// 但這個函式執行完之後往往就會 consolidate() 整理格式,所以不會有差。
|
||||
writeFile.seek(toFileOffset: fileSize - 1)
|
||||
if writeFile.readDataToEndOfFile().first != 0x0A {
|
||||
vCLMLog("EOF Missing Confirmed, Start Fixing.")
|
||||
vCLog("EOF Missing Confirmed, Start Fixing.")
|
||||
var newData = Data()
|
||||
newData.append(0x0A)
|
||||
writeFile.write(newData)
|
||||
vCLMLog("EOF Successfully Assured.")
|
||||
vCLog("EOF Successfully Assured.")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -141,29 +142,14 @@ public extension LMAssembly {
|
|||
// Write consolidated file contents.
|
||||
try strProcessed.write(to: urlPath, atomically: false, encoding: .utf8)
|
||||
} catch {
|
||||
vCLMLog("Consolidation Failed w/ File: \(path), error: \(error)")
|
||||
vCLog("Consolidation Failed w/ File: \(path), error: \(error)")
|
||||
return false
|
||||
}
|
||||
vCLMLog("Either Consolidation Successful Or No-Need-To-Consolidate.")
|
||||
vCLog("Either Consolidation Successful Or No-Need-To-Consolidate.")
|
||||
return true
|
||||
}
|
||||
vCLMLog("Consolidation Failed: File Missing at \(path).")
|
||||
vCLog("Consolidation Failed: File Missing at \(path).")
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
mutating func regReplace(pattern: String, replaceWith: String = "") {
|
||||
// Ref: https://stackoverflow.com/a/40993403/4162914 && https://stackoverflow.com/a/71291137/4162914
|
||||
do {
|
||||
let regex = try NSRegularExpression(
|
||||
pattern: pattern, options: [.caseInsensitive, .anchorsMatchLines]
|
||||
)
|
||||
let range = NSRange(startIndex..., in: self)
|
||||
self = regex.stringByReplacingMatches(
|
||||
in: self, options: [], range: range, withTemplate: replaceWith
|
||||
)
|
||||
} catch { return }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,8 +8,9 @@
|
|||
|
||||
import Foundation
|
||||
import Megrez
|
||||
import Shared
|
||||
|
||||
public extension LMAssembly {
|
||||
public extension vChewingLM {
|
||||
/// 語言模組副本化模組(LMInstantiator,下稱「LMI」)自身為符合天權星組字引擎內
|
||||
/// 的 LangModelProtocol 協定的模組、統籌且整理來自其它子模組的資料(包括使
|
||||
/// 用者語彙、繪文字模組、語彙濾除表、原廠語言模組等)。
|
||||
|
@ -28,49 +29,18 @@ public extension LMAssembly {
|
|||
/// LMI 會根據需要分別載入原廠語言模組和其他個別的子語言模組。LMI 本身不會記錄這些
|
||||
/// 語言模組的相關資料的存放位置,僅藉由參數來讀取相關訊息。
|
||||
class LMInstantiator: LangModelProtocol {
|
||||
public struct Config {
|
||||
/// 如果設定為 nil 的話,則不產生任何詞頻資料。
|
||||
/// true = 全形,false = 半形。
|
||||
public var numPadFWHWStatus: Bool?
|
||||
public var isCassetteEnabled = false
|
||||
public var isPhraseReplacementEnabled = false
|
||||
public var isCNSEnabled = false
|
||||
public var isSymbolEnabled = false
|
||||
public var isSCPCEnabled = false
|
||||
public var filterNonCNSReadings = false
|
||||
public var deltaOfCalendarYears: Int = -2000
|
||||
}
|
||||
|
||||
public static var asyncLoadingUserData: Bool = true
|
||||
|
||||
// SQLite 連線所在的記憶體位置。
|
||||
static var ptrSQL: OpaquePointer?
|
||||
|
||||
// SQLite 連線是否已經建立。
|
||||
public internal(set) static var isSQLDBConnected: Bool = false
|
||||
|
||||
// 簡體中文模型?
|
||||
public let isCHS: Bool
|
||||
|
||||
// 在函式內部用以記錄狀態的開關。
|
||||
public private(set) var config = Config()
|
||||
public var isCassetteEnabled = false
|
||||
public var isPhraseReplacementEnabled = false
|
||||
public var isCNSEnabled = false
|
||||
public var isSymbolEnabled = false
|
||||
public var isSCPCEnabled = false
|
||||
public var isCHS = false
|
||||
public var deltaOfCalendarYears: Int = -2000
|
||||
|
||||
// 這句需要留著,不然無法被 package 外界存取。
|
||||
public init(
|
||||
isCHS: Bool = false,
|
||||
uomDataURL: URL? = nil
|
||||
) {
|
||||
public init(isCHS: Bool = false) {
|
||||
self.isCHS = isCHS
|
||||
lmUserOverride = .init(dataURL: uomDataURL)
|
||||
}
|
||||
|
||||
@discardableResult public func setOptions(handler: (inout Config) -> Void) -> LMInstantiator {
|
||||
handler(&config)
|
||||
return self
|
||||
}
|
||||
|
||||
public static func setCassetCandidateKeyValidator(_ validator: @escaping (String) -> Bool) {
|
||||
Self.lmCassette.candidateKeysValidator = validator
|
||||
}
|
||||
|
||||
/// 介紹一下幾個通用的語言模組型別:
|
||||
|
@ -79,13 +49,31 @@ public extension LMAssembly {
|
|||
/// 比較適合那種每筆記錄都有不同的權重數值的語言模組,雖然也可以強制施加權重數值就是了。
|
||||
/// LMCoreEX 的辭典陣列不承載 Unigram 本體、而是承載索引範圍,這樣可以節約記憶體。
|
||||
/// 一個 LMCoreEX 就可以滿足威注音幾乎所有語言模組副本的需求,當然也有這兩個例外:
|
||||
/// LMReplacements 與 LMAssociates 分別擔當語彙置換表資料與使用者關聯詞語的資料承載工作。
|
||||
/// LMReplacements 與 LMAssociates 分別擔當語彙置換表資料與使用者聯想詞的資料承載工作。
|
||||
/// 但是,LMCoreEX 對 2010-2013 年等舊 mac 機種而言,讀取速度異常緩慢。
|
||||
/// 於是 LMCoreJSON 就出場了,專門用來讀取原廠的 JSON 格式的辭典。
|
||||
|
||||
// 聲明原廠語言模組:
|
||||
// Reverse 的話,第一欄是注音,第二欄是對應的漢字,第三欄是可能的權重。
|
||||
// 不 Reverse 的話,第一欄是漢字,第二欄是對應的注音,第三欄是可能的權重。
|
||||
var lmCore = LMCoreJSON(
|
||||
reverse: false, consolidate: false, defaultScore: -9.9, forceDefaultScore: false
|
||||
)
|
||||
var lmMisc = LMCoreJSON(
|
||||
reverse: true, consolidate: false, defaultScore: -1.0, forceDefaultScore: false
|
||||
)
|
||||
|
||||
// 簡體中文模式與繁體中文模式共用全字庫擴展模組,故靜態處理。
|
||||
// 不然,每個模式都會讀入一份全字庫,會多佔用 100MB 記憶體。
|
||||
static var lmCNS = vChewingLM.LMCoreJSON(
|
||||
reverse: true, consolidate: false, defaultScore: -11.0, forceDefaultScore: false
|
||||
)
|
||||
static var lmSymbols = vChewingLM.LMCoreJSON(
|
||||
reverse: true, consolidate: false, defaultScore: -13.0, forceDefaultScore: false
|
||||
)
|
||||
|
||||
// 磁帶資料模組。「currentCassette」對外唯讀,僅用來讀取磁帶本身的中繼資料(Metadata)。
|
||||
static var lmCassette = LMCassette()
|
||||
static var lmPlainBopomofo = LMPlainBopomofo()
|
||||
|
||||
// 聲明使用者語言模組。
|
||||
// 使用者語言模組使用多執行緒的話,可能會導致一些問題。有時間再仔細排查看看。
|
||||
|
@ -100,46 +88,77 @@ public extension LMAssembly {
|
|||
)
|
||||
var lmReplacements = LMReplacements()
|
||||
var lmAssociates = LMAssociates()
|
||||
|
||||
// 半衰记忆模组
|
||||
var lmUserOverride: LMUserOverride
|
||||
var lmPlainBopomofo = LMPlainBopomofo()
|
||||
|
||||
// MARK: - 工具函式
|
||||
|
||||
public func resetFactoryJSONModels() {}
|
||||
public func resetFactoryJSONModels() {
|
||||
lmCore.clear()
|
||||
lmMisc.clear()
|
||||
Self.lmCNS.clear()
|
||||
Self.lmSymbols.clear()
|
||||
}
|
||||
|
||||
public var isCoreLMLoaded: Bool { lmCore.isLoaded }
|
||||
public func loadLanguageModel(json: (dict: [String: [String]]?, path: String)) {
|
||||
guard let jsonDict = json.dict else {
|
||||
vCLog("lmCore: File access failure: \(json.path)")
|
||||
return
|
||||
}
|
||||
lmCore.load((dict: jsonDict, path: json.path))
|
||||
vCLog("lmCore: \(lmCore.count) entries of data loaded from: \(json.path)")
|
||||
}
|
||||
|
||||
public var isCNSDataLoaded: Bool { Self.lmCNS.isLoaded }
|
||||
public func loadCNSData(json: (dict: [String: [String]]?, path: String)) {
|
||||
guard let jsonDict = json.dict else {
|
||||
vCLog("lmCNS: File access failure: \(json.path)")
|
||||
return
|
||||
}
|
||||
Self.lmCNS.load((dict: jsonDict, path: json.path))
|
||||
vCLog("lmCNS: \(Self.lmCNS.count) entries of data loaded from: \(json.path)")
|
||||
}
|
||||
|
||||
public var isMiscDataLoaded: Bool { lmMisc.isLoaded }
|
||||
public func loadMiscData(json: (dict: [String: [String]]?, path: String)) {
|
||||
guard let jsonDict = json.dict else {
|
||||
vCLog("lmCore: File access failure: \(json.path)")
|
||||
return
|
||||
}
|
||||
lmMisc.load((dict: jsonDict, path: json.path))
|
||||
vCLog("lmMisc: \(lmMisc.count) entries of data loaded from: \(json.path)")
|
||||
}
|
||||
|
||||
public var isSymbolDataLoaded: Bool { Self.lmSymbols.isLoaded }
|
||||
public func loadSymbolData(json: (dict: [String: [String]]?, path: String)) {
|
||||
guard let jsonDict = json.dict else {
|
||||
vCLog("lmCore: File access failure: \(json.path)")
|
||||
return
|
||||
}
|
||||
Self.lmSymbols.load((dict: jsonDict, path: json.path))
|
||||
vCLog("lmSymbols: \(Self.lmSymbols.count) entries of data loaded from: \(json.path)")
|
||||
}
|
||||
|
||||
// 上述幾個函式不要加 Async,因為這些內容都被 LMMgr 負責用別的方法 Async 了、用 GCD 的多任務並行共結來完成。
|
||||
|
||||
public func loadUserPhrasesData(path: String, filterPath: String?) {
|
||||
func loadMain() {
|
||||
DispatchQueue.main.async {
|
||||
if FileManager.default.isReadableFile(atPath: path) {
|
||||
lmUserPhrases.clear()
|
||||
lmUserPhrases.open(path)
|
||||
vCLMLog("lmUserPhrases: \(lmUserPhrases.count) entries of data loaded from: \(path)")
|
||||
self.lmUserPhrases.clear()
|
||||
self.lmUserPhrases.open(path)
|
||||
vCLog("lmUserPhrases: \(self.lmUserPhrases.count) entries of data loaded from: \(path)")
|
||||
} else {
|
||||
vCLMLog("lmUserPhrases: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
if !Self.asyncLoadingUserData {
|
||||
loadMain()
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
loadMain()
|
||||
vCLog("lmUserPhrases: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
guard let filterPath = filterPath else { return }
|
||||
func loadFilter() {
|
||||
DispatchQueue.main.async {
|
||||
if FileManager.default.isReadableFile(atPath: filterPath) {
|
||||
lmFiltered.clear()
|
||||
lmFiltered.open(filterPath)
|
||||
vCLMLog("lmFiltered: \(lmFiltered.count) entries of data loaded from: \(path)")
|
||||
self.lmFiltered.clear()
|
||||
self.lmFiltered.open(filterPath)
|
||||
vCLog("lmFiltered: \(self.lmFiltered.count) entries of data loaded from: \(path)")
|
||||
} else {
|
||||
vCLMLog("lmFiltered: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
if !Self.asyncLoadingUserData {
|
||||
loadFilter()
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
loadFilter()
|
||||
vCLog("lmFiltered: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -149,85 +168,69 @@ public extension LMAssembly {
|
|||
if FileManager.default.isReadableFile(atPath: path) {
|
||||
lmFiltered.clear()
|
||||
lmFiltered.open(path)
|
||||
vCLMLog("lmFiltered: \(lmFiltered.count) entries of data loaded from: \(path)")
|
||||
vCLog("lmFiltered: \(lmFiltered.count) entries of data loaded from: \(path)")
|
||||
} else {
|
||||
vCLMLog("lmFiltered: File access failure: \(path)")
|
||||
vCLog("lmFiltered: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
|
||||
public func loadUserSymbolData(path: String) {
|
||||
func load() {
|
||||
DispatchQueue.main.async {
|
||||
if FileManager.default.isReadableFile(atPath: path) {
|
||||
lmUserSymbols.clear()
|
||||
lmUserSymbols.open(path)
|
||||
vCLMLog("lmUserSymbol: \(lmUserSymbols.count) entries of data loaded from: \(path)")
|
||||
self.lmUserSymbols.clear()
|
||||
self.lmUserSymbols.open(path)
|
||||
vCLog("lmUserSymbol: \(self.lmUserSymbols.count) entries of data loaded from: \(path)")
|
||||
} else {
|
||||
vCLMLog("lmUserSymbol: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
if !Self.asyncLoadingUserData {
|
||||
load()
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
load()
|
||||
vCLog("lmUserSymbol: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func loadUserAssociatesData(path: String) {
|
||||
func load() {
|
||||
DispatchQueue.main.async {
|
||||
if FileManager.default.isReadableFile(atPath: path) {
|
||||
lmAssociates.clear()
|
||||
lmAssociates.open(path)
|
||||
vCLMLog("lmAssociates: \(lmAssociates.count) entries of data loaded from: \(path)")
|
||||
self.lmAssociates.clear()
|
||||
self.lmAssociates.open(path)
|
||||
vCLog("lmAssociates: \(self.lmAssociates.count) entries of data loaded from: \(path)")
|
||||
} else {
|
||||
vCLMLog("lmAssociates: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
if !Self.asyncLoadingUserData {
|
||||
load()
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
load()
|
||||
vCLog("lmAssociates: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func loadReplacementsData(path: String) {
|
||||
func load() {
|
||||
DispatchQueue.main.async {
|
||||
if FileManager.default.isReadableFile(atPath: path) {
|
||||
lmReplacements.clear()
|
||||
lmReplacements.open(path)
|
||||
vCLMLog("lmReplacements: \(lmReplacements.count) entries of data loaded from: \(path)")
|
||||
self.lmReplacements.clear()
|
||||
self.lmReplacements.open(path)
|
||||
vCLog("lmReplacements: \(self.lmReplacements.count) entries of data loaded from: \(path)")
|
||||
} else {
|
||||
vCLMLog("lmReplacements: File access failure: \(path)")
|
||||
vCLog("lmReplacements: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
if !Self.asyncLoadingUserData {
|
||||
load()
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
load()
|
||||
}
|
||||
|
||||
public func loadSCPCSequencesData(path: String) {
|
||||
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 static func loadCassetteData(path: String) {
|
||||
func load() {
|
||||
DispatchQueue.main.async {
|
||||
if FileManager.default.isReadableFile(atPath: path) {
|
||||
Self.lmCassette.clear()
|
||||
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 {
|
||||
vCLMLog("lmCassette: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
if !Self.asyncLoadingUserData {
|
||||
load()
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
load()
|
||||
vCLog("lmCassette: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -308,9 +311,6 @@ public extension LMAssembly {
|
|||
/// - Returns: 是否在庫。
|
||||
public func hasUnigramsFor(keyArray: [String]) -> Bool {
|
||||
let keyChain = keyArray.joined(separator: "-")
|
||||
// 因為涉及到對濾除清單的檢查,所以這裡必須走一遍 .unigramsFor()。
|
||||
// 從 SQL 查詢的角度來看,這樣恐怕不是很經濟,因為 SQLite 要專門準備一次查詢結果。
|
||||
// 但以 2010 年的電腦效能作為基準參考來看的話,這方面的效能壓力可以忽略不計。
|
||||
return keyChain == " " || (!unigramsFor(keyArray: keyArray).isEmpty && !keyChain.isEmpty)
|
||||
}
|
||||
|
||||
|
@ -322,7 +322,7 @@ public extension LMAssembly {
|
|||
/// - Returns: 是否在庫。
|
||||
public func hasKeyValuePairFor(keyArray: [String], value: String, factoryDictionaryOnly: Bool = false) -> Bool {
|
||||
factoryDictionaryOnly
|
||||
? factoryCoreUnigramsFor(key: keyArray.joined(separator: "-")).map(\.value).contains(value)
|
||||
? lmCore.unigramsFor(key: keyArray.joined(separator: "-")).map(\.value).contains(value)
|
||||
: unigramsFor(keyArray: keyArray).map(\.value).contains(value)
|
||||
}
|
||||
|
||||
|
@ -333,7 +333,7 @@ public extension LMAssembly {
|
|||
/// - Returns: 是否在庫。
|
||||
public func countKeyValuePairs(keyArray: [String], factoryDictionaryOnly: Bool = false) -> Int {
|
||||
factoryDictionaryOnly
|
||||
? factoryCoreUnigramsFor(key: keyArray.joined(separator: "-")).count
|
||||
? lmCore.unigramsFor(key: keyArray.joined(separator: "-")).count
|
||||
: unigramsFor(keyArray: keyArray).count
|
||||
}
|
||||
|
||||
|
@ -349,79 +349,41 @@ public extension LMAssembly {
|
|||
/// 準備不同的語言模組容器,開始逐漸往容器陣列內塞入資料。
|
||||
var rawAllUnigrams: [Megrez.Unigram] = []
|
||||
|
||||
if config.isCassetteEnabled { rawAllUnigrams += Self.lmCassette.unigramsFor(key: keyChain) }
|
||||
if isCassetteEnabled { rawAllUnigrams += Self.lmCassette.unigramsFor(key: keyChain) }
|
||||
|
||||
// 如果有檢測到使用者自訂逐字選字語料庫內的相關資料的話,在這裡先插入。
|
||||
if config.isSCPCEnabled {
|
||||
rawAllUnigrams += Self.lmPlainBopomofo.valuesFor(key: keyChain, isCHS: isCHS).map {
|
||||
Megrez.Unigram(value: $0, score: 0)
|
||||
}
|
||||
}
|
||||
|
||||
if !config.isCassetteEnabled || config.isCassetteEnabled && keyChain.map(\.description)[0] == "_" {
|
||||
// 先給出 NumPad 的結果。
|
||||
rawAllUnigrams += supplyNumPadUnigrams(key: keyChain)
|
||||
// LMMisc 與 LMCore 的 score 在 (-10.0, 0.0) 這個區間內。
|
||||
rawAllUnigrams += factoryUnigramsFor(key: keyChain, column: .theDataCHEW)
|
||||
// 原廠核心辭典內容。
|
||||
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 {
|
||||
rawAllUnigrams += factoryUnigramsFor(key: keyChain, column: .theDataCNS)
|
||||
}
|
||||
}
|
||||
|
||||
if config.isSymbolEnabled {
|
||||
rawAllUnigrams += lmUserSymbols.unigramsFor(key: keyChain)
|
||||
if !config.isCassetteEnabled {
|
||||
rawAllUnigrams += factoryUnigramsFor(key: keyChain, column: .theDataSYMB)
|
||||
}
|
||||
if isSCPCEnabled {
|
||||
rawAllUnigrams += lmPlainBopomofo.valuesFor(key: keyChain).map { Megrez.Unigram(value: $0, score: 0) }
|
||||
}
|
||||
|
||||
// 用 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 += lmUserPhrases.unigramsFor(key: keyChain).reversed()
|
||||
|
||||
if !isCassetteEnabled || isCassetteEnabled && keyChain.map(\.description)[0] == "_" {
|
||||
// LMMisc 與 LMCore 的 score 在 (-10.0, 0.0) 這個區間內。
|
||||
rawAllUnigrams += lmMisc.unigramsFor(key: keyChain)
|
||||
rawAllUnigrams += lmCore.unigramsFor(key: keyChain)
|
||||
if isCNSEnabled { rawAllUnigrams += Self.lmCNS.unigramsFor(key: keyChain) }
|
||||
}
|
||||
|
||||
if isSymbolEnabled {
|
||||
rawAllUnigrams += lmUserSymbols.unigramsFor(key: keyChain)
|
||||
if !isCassetteEnabled {
|
||||
rawAllUnigrams += Self.lmSymbols.unigramsFor(key: keyChain)
|
||||
}
|
||||
}
|
||||
rawAllUnigrams = userPhraseUnigrams + rawAllUnigrams
|
||||
|
||||
// 分析且處理可能存在的 InputToken。
|
||||
rawAllUnigrams = rawAllUnigrams.map { unigram in
|
||||
let convertedValues = unigram.value.parseAsInputToken(isCHS: isCHS)
|
||||
guard !convertedValues.isEmpty else { return [unigram] }
|
||||
var result = [Megrez.Unigram]()
|
||||
convertedValues.enumerated().forEach { absDelta, value in
|
||||
let newScore: Double = -80 - Double(absDelta) * 0.01
|
||||
result.append(.init(value: value, score: newScore))
|
||||
}
|
||||
return result
|
||||
}.flatMap { $0 }
|
||||
|
||||
// 新增與日期、時間、星期有關的單元圖資料。
|
||||
// 新增與日期、時間、星期有關的單元圖資料
|
||||
rawAllUnigrams.append(contentsOf: queryDateTimeUnigrams(with: keyChain))
|
||||
|
||||
if keyChain == "_punctuation_list" {
|
||||
rawAllUnigrams.append(contentsOf: getHaninSymbolMenuUnigrams())
|
||||
rawAllUnigrams.append(contentsOf: lmCore.getHaninSymbolMenuUnigrams())
|
||||
}
|
||||
|
||||
// 提前處理語彙置換。
|
||||
if config.isPhraseReplacementEnabled {
|
||||
// 提前處理語彙置換
|
||||
if isPhraseReplacementEnabled {
|
||||
for i in 0 ..< rawAllUnigrams.count {
|
||||
let newValue = lmReplacements.valuesFor(key: rawAllUnigrams[i].value)
|
||||
guard !newValue.isEmpty else { continue }
|
||||
|
|
|
@ -8,9 +8,9 @@
|
|||
|
||||
import Foundation
|
||||
import Megrez
|
||||
import SwiftExtension
|
||||
import Shared
|
||||
|
||||
public extension LMAssembly.LMInstantiator {
|
||||
public extension vChewingLM.LMInstantiator {
|
||||
/// 磁帶模式專用:當前磁帶所規定的花牌鍵。
|
||||
var cassetteWildcardKey: String { Self.lmCassette.wildcardKey }
|
||||
/// 磁帶模式專用:當前磁帶規定的最大碼長。
|
||||
|
@ -19,8 +19,6 @@ public extension LMAssembly.LMInstantiator {
|
|||
var nullCandidateInCassette: String { Self.lmCassette.nullCandidate }
|
||||
/// 磁帶模式專用:選字鍵是否需要敲 Shift 才會生效。
|
||||
var areCassetteCandidateKeysShiftHeld: Bool { Self.lmCassette.areCandidateKeysShiftHeld }
|
||||
/// 磁帶模式專用:需要直接递交的按键。
|
||||
var keysToDirectlyCommit: String { Self.lmCassette.keysToDirectlyCommit }
|
||||
/// 磁帶模式專用:選字鍵,在威注音輸入法當中僅優先用於快速模式。
|
||||
var cassetteSelectionKey: String? {
|
||||
let result = Self.lmCassette.selectionKeys
|
||||
|
|
|
@ -11,72 +11,87 @@ import Megrez
|
|||
|
||||
// MARK: - 日期時間便捷輸入功能
|
||||
|
||||
extension LMAssembly.LMInstantiator {
|
||||
extension vChewingLM.LMInstantiator {
|
||||
func queryDateTimeUnigrams(with key: String = "") -> [Megrez.Unigram] {
|
||||
guard let tokenTrigger = TokenTrigger(rawValue: key) else { return [] }
|
||||
if !["ㄖˋ-ㄑㄧ", "ㄖˋ-ㄑㄧˊ", "ㄕˊ-ㄐㄧㄢ", "ㄒㄧㄥ-ㄑㄧ", "ㄒㄧㄥ-ㄑㄧˊ"].contains(key) { return .init() }
|
||||
var results = [Megrez.Unigram]()
|
||||
var tokens: [String] = []
|
||||
|
||||
func processDateWithDayDelta(_ delta: Int) {
|
||||
tokens = ["MACRO@DATE_DAYDELTA:\(delta)"]
|
||||
if config.deltaOfCalendarYears != 0 { tokens.append("MACRO@DATE_DAYDELTA:\(delta)_YEARDELTA:\(config.deltaOfCalendarYears)") }
|
||||
tokens.append("MACRO@DATE_DAYDELTA:\(delta)_SHORTENED")
|
||||
tokens.append("MACRO@DATE_DAYDELTA:\(delta)_LUNA")
|
||||
}
|
||||
|
||||
func processYearWithYearDelta(_ delta: Int) {
|
||||
tokens = ["MACRO@YEAR_YEARDELTA:\(delta)"]
|
||||
if config.deltaOfCalendarYears != 0 { tokens.append("MACRO@YEAR_YEARDELTA:\(delta + config.deltaOfCalendarYears)") }
|
||||
tokens.append("MACRO@YEAR_GANZHI_YEARDELTA:\(delta)")
|
||||
tokens.append("MACRO@YEAR_ZODIAC_YEARDELTA:\(delta)")
|
||||
}
|
||||
|
||||
switch tokenTrigger {
|
||||
case .jin1tian1ri4qi2, .jin1tian1ri4qi1: processDateWithDayDelta(0) // 今天日期
|
||||
case .zuo2tian1ri4qi2, .zuo2tian1ri4qi1: processDateWithDayDelta(-1) // 昨天日期
|
||||
case .qian2tian1ri4qi2, .qian2tian1ri4qi1: processDateWithDayDelta(-2) // 前天日期
|
||||
case .ming2tian1ri4qi2, .ming2tian1ri4qi1: processDateWithDayDelta(1) // 明天日期
|
||||
case .hou4tian1ri4qi1, .hou4tian1ri4qi2: processDateWithDayDelta(2) // 後天日期
|
||||
case .jin1nian2nian2du4: processYearWithYearDelta(0) // 今年年度
|
||||
case .qu4nian2nian2du4: processYearWithYearDelta(-1) // 去年年度
|
||||
case .qian2nian2nian2du4: processYearWithYearDelta(-2) // 前年年度
|
||||
case .ming2nian2nian2du4: processYearWithYearDelta(1) // 明年年度
|
||||
case .hou4nian2nian2du4: processYearWithYearDelta(2) // 後年年度
|
||||
case .shi2jian1: tokens = ["MACRO@TIME_SHORTENED"] // 時間
|
||||
case .xing1qi1, .xing1qi2: tokens = ["MACRO@WEEK_SHORTENED", "MACRO@WEEK"] // 星期
|
||||
case .suo3zai4shi2qu1, .dang1qian2shi2qu1, .mu4qian2shi2qu1: tokens = ["MACRO@TIMEZONE", "MACRO@TIMEZONE_SHORTENED"] // 時區
|
||||
}
|
||||
// 終末處理。
|
||||
let values = tokens.map { $0.parseAsInputToken(isCHS: isCHS) }.flatMap { $0 }.deduplicated
|
||||
var i: Double = -99
|
||||
for strValue in values.reversed() {
|
||||
results.insert(.init(value: strValue, score: i), at: 0)
|
||||
i += 1
|
||||
let theLocale = Locale(identifier: "zh-Hant")
|
||||
let currentDate = Date()
|
||||
var delta = DateComponents()
|
||||
let thisYear = Calendar.current.dateComponents([.year], from: currentDate).year ?? 2018
|
||||
delta.year = max(min(deltaOfCalendarYears, 0), thisYear * -1)
|
||||
let currentDateShortened = Calendar.current.date(byAdding: delta, to: currentDate)
|
||||
switch key {
|
||||
case "ㄖˋ-ㄑㄧ", "ㄖˋ-ㄑㄧˊ":
|
||||
let formatterDate1 = DateFormatter()
|
||||
let formatterDate2 = DateFormatter()
|
||||
formatterDate1.dateFormat = "yyyy-MM-dd"
|
||||
formatterDate2.dateFormat = "yyyy年MM月dd日"
|
||||
let date1 = formatterDate1.string(from: currentDate)
|
||||
let date2 = formatterDate2.string(from: currentDate)
|
||||
var date3 = date2.convertArabicNumeralsToChinese
|
||||
date3 = date3.replacingOccurrences(of: "年〇", with: "年")
|
||||
date3 = date3.replacingOccurrences(of: "月〇", with: "月")
|
||||
results.append(.init(value: date1, score: -94))
|
||||
results.append(.init(value: date2, score: -95))
|
||||
results.append(.init(value: date3, score: -96))
|
||||
if let currentDateShortened = currentDateShortened, delta.year != 0 {
|
||||
var dateAlt1: String = formatterDate1.string(from: currentDateShortened)
|
||||
dateAlt1.regReplace(pattern: #"^0+"#)
|
||||
var dateAlt2: String = formatterDate2.string(from: currentDateShortened)
|
||||
dateAlt2.regReplace(pattern: #"^0+"#)
|
||||
var dateAlt3 = dateAlt2.convertArabicNumeralsToChinese
|
||||
dateAlt3 = dateAlt3.replacingOccurrences(of: "年〇", with: "年")
|
||||
dateAlt3 = dateAlt3.replacingOccurrences(of: "月〇", with: "月")
|
||||
results.append(.init(value: dateAlt1, score: -97))
|
||||
results.append(.init(value: dateAlt2, score: -98))
|
||||
results.append(.init(value: dateAlt3, score: -99))
|
||||
}
|
||||
case "ㄕˊ-ㄐㄧㄢ":
|
||||
let formatterTime1 = DateFormatter()
|
||||
let formatterTime2 = DateFormatter()
|
||||
let formatterTime3 = DateFormatter()
|
||||
formatterTime1.dateFormat = "HH:mm"
|
||||
formatterTime2.dateFormat = isCHS ? "HH点mm分" : "HH點mm分"
|
||||
formatterTime3.dateFormat = isCHS ? "HH时mm分" : "HH時mm分"
|
||||
let time1 = formatterTime1.string(from: currentDate)
|
||||
let time2 = formatterTime2.string(from: currentDate)
|
||||
let time3 = formatterTime3.string(from: currentDate)
|
||||
results.append(.init(value: time1, score: -97))
|
||||
results.append(.init(value: time2, score: -98))
|
||||
results.append(.init(value: time3, score: -99))
|
||||
case "ㄒㄧㄥ-ㄑㄧ", "ㄒㄧㄥ-ㄑㄧˊ":
|
||||
let formatterWeek1 = DateFormatter()
|
||||
let formatterWeek2 = DateFormatter()
|
||||
formatterWeek1.dateFormat = "EEEE"
|
||||
formatterWeek2.dateFormat = "EE"
|
||||
formatterWeek1.locale = theLocale
|
||||
formatterWeek2.locale = theLocale
|
||||
let week1 = formatterWeek1.string(from: currentDate)
|
||||
let week2 = formatterWeek2.string(from: currentDate)
|
||||
results.append(.init(value: week1, score: -98))
|
||||
results.append(.init(value: week2, score: -99))
|
||||
default: return .init()
|
||||
}
|
||||
return results
|
||||
}
|
||||
}
|
||||
|
||||
private enum TokenTrigger: String {
|
||||
case shi2jian1 = "ㄕˊ-ㄐㄧㄢ"
|
||||
case xing1qi1 = "ㄒㄧㄥ-ㄑㄧ"
|
||||
case xing1qi2 = "ㄒㄧㄥ-ㄑㄧˊ"
|
||||
case jin1nian2nian2du4 = "ㄐㄧㄣ-ㄋㄧㄢˊ-ㄋㄧㄢˊ-ㄉㄨˋ"
|
||||
case qu4nian2nian2du4 = "ㄑㄩˋ-ㄋㄧㄢˊ-ㄋㄧㄢˊ-ㄉㄨˋ"
|
||||
case ming2nian2nian2du4 = "ㄇㄧㄥˊ-ㄋㄧㄢˊ-ㄋㄧㄢˊ-ㄉㄨˋ"
|
||||
case qian2nian2nian2du4 = "ㄑㄧㄢˊ-ㄋㄧㄢˊ-ㄋㄧㄢˊ-ㄉㄨˋ"
|
||||
case hou4nian2nian2du4 = "ㄏㄡˋ-ㄋㄧㄢˊ-ㄋㄧㄢˊ-ㄉㄨˋ"
|
||||
case jin1tian1ri4qi2 = "ㄐㄧㄣ-ㄊㄧㄢ-ㄖˋ-ㄑㄧˊ"
|
||||
case ming2tian1ri4qi2 = "ㄇㄧㄥˊ-ㄊㄧㄢ-ㄖˋ-ㄑㄧˊ"
|
||||
case zuo2tian1ri4qi2 = "ㄗㄨㄛˊ-ㄊㄧㄢ-ㄖˋ-ㄑㄧˊ"
|
||||
case qian2tian1ri4qi2 = "ㄑㄧㄢˊ-ㄊㄧㄢ-ㄖˋ-ㄑㄧˊ"
|
||||
case hou4tian1ri4qi2 = "ㄏㄡˋ-ㄊㄧㄢ-ㄖˋ-ㄑㄧˊ"
|
||||
case jin1tian1ri4qi1 = "ㄐㄧㄣ-ㄊㄧㄢ-ㄖˋ-ㄑㄧ"
|
||||
case ming2tian1ri4qi1 = "ㄇㄧㄥˊ-ㄊㄧㄢ-ㄖˋ-ㄑㄧ"
|
||||
case zuo2tian1ri4qi1 = "ㄗㄨㄛˊ-ㄊㄧㄢ-ㄖˋ-ㄑㄧ"
|
||||
case qian2tian1ri4qi1 = "ㄑㄧㄢˊ-ㄊㄧㄢ-ㄖˋ-ㄑㄧ"
|
||||
case hou4tian1ri4qi1 = "ㄏㄡˋ-ㄊㄧㄢ-ㄖˋ-ㄑㄧ"
|
||||
case dang1qian2shi2qu1 = "ㄉㄤ-ㄑㄧㄢˊ-ㄕˊ-ㄑㄩ"
|
||||
case mu4qian2shi2qu1 = "ㄇㄨˋ-ㄑㄧㄢˊ-ㄕˊ-ㄑㄩ"
|
||||
case suo3zai4shi2qu1 = "ㄙㄨㄛˇ-ㄗㄞˋ-ㄕˊ-ㄑㄩ"
|
||||
// MARK: - Date Time Language Conversion Extension
|
||||
|
||||
private let tableMappingArabicNumeralsToChinese: [String: String] = [
|
||||
"0": "〇", "1": "一", "2": "二", "3": "三", "4": "四", "5": "五", "6": "六", "7": "七", "8": "八", "9": "九",
|
||||
]
|
||||
|
||||
private extension String {
|
||||
/// 將給定的字串當中的阿拉伯數字轉為漢語小寫,逐字轉換。
|
||||
/// - Parameter target: 要進行轉換操作的對象,會直接修改該對象。
|
||||
var convertArabicNumeralsToChinese: String {
|
||||
var target = self
|
||||
for key in tableMappingArabicNumeralsToChinese.keys {
|
||||
guard let result = tableMappingArabicNumeralsToChinese[key] else { continue }
|
||||
target = target.replacingOccurrences(of: key, with: result)
|
||||
}
|
||||
return target
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +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 supplyNumPadUnigrams(key: String) -> [Megrez.Unigram] {
|
||||
guard let status = config.numPadFWHWStatus else { return [] }
|
||||
let initials = "_NumPad_"
|
||||
guard key.hasPrefix(initials) else { return [] }
|
||||
let char = key.replacingOccurrences(of: initials, with: "")
|
||||
guard char.count == 1 else { return [] }
|
||||
let gram1 = Megrez.Unigram(value: char.applyingTransformFW2HW(reverse: status), score: 0)
|
||||
let gram2 = Megrez.Unigram(value: char.applyingTransformFW2HW(reverse: !status), score: -0.1)
|
||||
return [gram1, gram2]
|
||||
}
|
||||
}
|
|
@ -1,295 +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
|
||||
import SQLite3
|
||||
|
||||
/* ==============
|
||||
因應 Apple 對 8GB 運行記憶體的病態偏執,威注音的原廠辭典格式更換為 SQLite、以圖減少對記憶體的佔用。
|
||||
資料結構如下:
|
||||
CREATE TABLE IF NOT EXISTS DATA_MAIN (
|
||||
theKey TEXT NOT NULL,
|
||||
theDataCHS TEXT,
|
||||
theDataCHT TEXT,
|
||||
theDataCNS TEXT,
|
||||
theDataMISC TEXT,
|
||||
theDataSYMB TEXT,
|
||||
theDataCHEW TEXT,
|
||||
PRIMARY KEY (theKey)
|
||||
) WITHOUT ROWID;
|
||||
CREATE TABLE IF NOT EXISTS DATA_REV (
|
||||
theChar TEXT NOT NULL,
|
||||
theReadings TEXT NOT NULL,
|
||||
PRIMARY KEY (theChar)
|
||||
) WITHOUT ROWID;
|
||||
*/
|
||||
|
||||
extension LMAssembly.LMInstantiator {
|
||||
enum CoreColumn: Int32 {
|
||||
case theDataCHS = 1 // 簡體中文
|
||||
case theDataCHT = 2 // 繁體中文
|
||||
case theDataCNS = 3 // 全字庫
|
||||
case theDataMISC = 4 // 待辦
|
||||
case theDataSYMB = 5 // 符號圖
|
||||
case theDataCHEW = 6 // 注音文
|
||||
|
||||
var name: String { String(describing: self) }
|
||||
|
||||
var id: Int32 { rawValue }
|
||||
|
||||
var defaultScore: Double {
|
||||
switch self {
|
||||
case .theDataCHEW: return -1
|
||||
case .theDataCNS: return -11
|
||||
case .theDataSYMB: return -13
|
||||
case .theDataMISC: return -10
|
||||
default: return -9.9
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension LMAssembly.LMInstantiator {
|
||||
@discardableResult public static func connectSQLDB(dbPath: String, dropPreviousConnection: Bool = true) -> Bool {
|
||||
if dropPreviousConnection { disconnectSQLDB() }
|
||||
vCLMLog("Establishing SQLite connection to: \(dbPath)")
|
||||
guard sqlite3_open(dbPath, &Self.ptrSQL) == SQLITE_OK else { return false }
|
||||
guard "PRAGMA journal_mode = OFF;".runAsSQLExec(dbPointer: &ptrSQL) else { return false }
|
||||
isSQLDBConnected = true
|
||||
return true
|
||||
}
|
||||
|
||||
public static func disconnectSQLDB() {
|
||||
if Self.ptrSQL != nil {
|
||||
sqlite3_close_v2(Self.ptrSQL)
|
||||
Self.ptrSQL = nil
|
||||
}
|
||||
isSQLDBConnected = false
|
||||
}
|
||||
|
||||
fileprivate static func querySQL(strStmt sqlQuery: String, coreColumn column: CoreColumn, handler: (String) -> Void) {
|
||||
guard Self.ptrSQL != nil else { return }
|
||||
performStatementSansResult { ptrStatement in
|
||||
sqlite3_prepare_v2(Self.ptrSQL, sqlQuery, -1, &ptrStatement, nil)
|
||||
while sqlite3_step(ptrStatement) == SQLITE_ROW {
|
||||
guard let rawValue = sqlite3_column_text(ptrStatement, column.id) else { continue }
|
||||
handler(String(cString: rawValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate static func hasSQLResult(strStmt sqlQuery: String) -> Bool {
|
||||
guard Self.ptrSQL != nil else { return false }
|
||||
var sqlQuery = sqlQuery
|
||||
if sqlQuery.last == ";" { sqlQuery = sqlQuery.dropLast(1).description } // 防呆設計。
|
||||
guard !sqlQuery.isEmpty else { return false }
|
||||
return performStatement { ptrStatement in
|
||||
let wrappedQuery = "SELECT EXISTS(\(sqlQuery));"
|
||||
sqlite3_prepare_v2(Self.ptrSQL, wrappedQuery, -1, &ptrStatement, nil)
|
||||
while sqlite3_step(ptrStatement) == SQLITE_ROW {
|
||||
return sqlite3_column_int(ptrStatement, 0) == 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取字根反查资料。
|
||||
public static func getFactoryReverseLookupData(with kanji: String) -> [String]? {
|
||||
var results: [String] = []
|
||||
let sqlQuery = "SELECT * FROM DATA_REV WHERE theChar='\(kanji)';"
|
||||
guard Self.ptrSQL != nil else { return nil }
|
||||
performStatementSansResult { ptrStatement in
|
||||
sqlite3_prepare_v2(Self.ptrSQL, sqlQuery, -1, &ptrStatement, nil)
|
||||
while sqlite3_step(ptrStatement) == SQLITE_ROW {
|
||||
guard let rawValue = sqlite3_column_text(ptrStatement, 1) else { continue }
|
||||
results.append(
|
||||
contentsOf: String(cString: rawValue).split(separator: "\t").map { reading in
|
||||
Self.restorePhonabetFromASCII(reading.description)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
return results.isEmpty ? nil : results
|
||||
}
|
||||
|
||||
func getHaninSymbolMenuUnigrams() -> [Megrez.Unigram] {
|
||||
let column: CoreColumn = isCHS ? .theDataCHS : .theDataCHT
|
||||
var grams: [Megrez.Unigram] = []
|
||||
let sqlQuery = "SELECT * FROM DATA_MAIN WHERE theKey='_punctuation_list';"
|
||||
Self.querySQL(strStmt: sqlQuery, coreColumn: column) { currentResult in
|
||||
let arrRangeRecords = currentResult.split(separator: "\t")
|
||||
for strNetaSet in arrRangeRecords {
|
||||
let neta = Array(strNetaSet.trimmingCharacters(in: .newlines).split(separator: " ").reversed())
|
||||
let theValue: String = .init(neta[0])
|
||||
var theScore = column.defaultScore
|
||||
if neta.count >= 2, let thisScore = Double(String(neta[1])) {
|
||||
theScore = thisScore
|
||||
}
|
||||
if theScore > 0 {
|
||||
theScore *= -1 // 應對可能忘記寫負號的情形
|
||||
}
|
||||
grams.append(Megrez.Unigram(value: theValue, score: theScore))
|
||||
}
|
||||
}
|
||||
return grams
|
||||
}
|
||||
|
||||
/// 根據給定的讀音索引鍵,來獲取原廠標準資料庫辭典內的對應資料陣列的 UTF8 資料、就地分析、生成單元圖陣列。
|
||||
/// - Remark: 該函式會無損地返回原廠辭典的結果,不受使用者控頻與資料過濾條件的影響,不包含全字庫的資料。
|
||||
/// - parameters:
|
||||
/// - key: 讀音索引鍵。
|
||||
public func factoryCoreUnigramsFor(key: String) -> [Megrez.Unigram] {
|
||||
// 此處需要把 ASCII 單引號換成連續兩個單引號,否則會有 SQLite 語句查詢故障。
|
||||
factoryUnigramsFor(key: key, column: isCHS ? .theDataCHS : .theDataCHT)
|
||||
}
|
||||
|
||||
/// 根據給定的讀音索引鍵,來獲取原廠標準資料庫辭典內的對應資料陣列的 UTF8 資料、就地分析、生成單元圖陣列。
|
||||
/// - parameters:
|
||||
/// - key: 讀音索引鍵。
|
||||
/// - column: 資料欄位。
|
||||
func factoryUnigramsFor(
|
||||
key: String, column: LMAssembly.LMInstantiator.CoreColumn
|
||||
) -> [Megrez.Unigram] {
|
||||
if key == "_punctuation_list" { return [] }
|
||||
var grams: [Megrez.Unigram] = []
|
||||
var gramsHW: [Megrez.Unigram] = []
|
||||
// 此處需要把 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
|
||||
var i: Double = 0
|
||||
var previousScore: Double?
|
||||
currentResult.split(separator: "\t").forEach { strNetaSet in
|
||||
// 這裡假定原廠資料已經經過對權重的 stable sort 排序。
|
||||
let neta = Array(strNetaSet.trimmingCharacters(in: .newlines).split(separator: " ").reversed())
|
||||
let theValue: String = .init(neta[0])
|
||||
var theScore = column.defaultScore
|
||||
if neta.count >= 2, let thisScore = Double(String(neta[1])) {
|
||||
theScore = thisScore
|
||||
}
|
||||
if theScore > 0 {
|
||||
theScore *= -1 // 應對可能忘記寫負號的情形
|
||||
}
|
||||
if previousScore == theScore {
|
||||
theScore -= i * 0.000_001
|
||||
i += 1
|
||||
} else {
|
||||
previousScore = theScore
|
||||
i = 0
|
||||
}
|
||||
grams.append(Megrez.Unigram(value: theValue, score: theScore))
|
||||
if !key.contains("_punctuation") { return }
|
||||
let halfValue = theValue.applyingTransformFW2HW(reverse: false)
|
||||
if halfValue != theValue {
|
||||
gramsHW.append(Megrez.Unigram(value: halfValue, score: theScore))
|
||||
}
|
||||
}
|
||||
}
|
||||
grams.append(contentsOf: gramsHW)
|
||||
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 資料、就地分析、生成單元圖陣列。
|
||||
/// - remark: 該函式暫時用不到,但先不用刪除。沒準今後會有用場。
|
||||
/// - parameters:
|
||||
/// - key: 讀音索引鍵。
|
||||
func hasFactoryCoreUnigramsFor(keyArray: [String]) -> Bool {
|
||||
let column: CoreColumn = isCHS ? .theDataCHS : .theDataCHT
|
||||
// 此處需要把 ASCII 單引號換成連續兩個單引號,否則會有 SQLite 語句查詢故障。
|
||||
let encryptedKey = Self.cnvPhonabetToASCII(keyArray.joined(separator: "-").replacingOccurrences(of: "'", with: "''"))
|
||||
// 此處為特例,無須以分號結尾。回頭整句塞到「SELECT EXISTS();」當中執行。
|
||||
let sqlQuery = "SELECT * FROM DATA_MAIN WHERE theKey='\(encryptedKey)' AND \(column.name) IS NOT NULL"
|
||||
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 {
|
||||
/// 內部函式,用以將注音讀音索引鍵進行加密。
|
||||
///
|
||||
/// 使用這種加密字串作為索引鍵,可以增加對 json 資料庫的存取速度。
|
||||
///
|
||||
/// 如果傳入的字串當中包含 ASCII 下畫線符號的話,則表明該字串並非注音讀音字串,會被忽略處理。
|
||||
/// - parameters:
|
||||
/// - incoming: 傳入的未加密注音讀音字串。
|
||||
static func cnvPhonabetToASCII(_ incoming: String) -> String {
|
||||
var strOutput = incoming
|
||||
if !strOutput.contains("_") {
|
||||
for entry in Self.dicPhonabet2ASCII {
|
||||
strOutput = strOutput.replacingOccurrences(of: entry.key, with: entry.value)
|
||||
}
|
||||
}
|
||||
return strOutput
|
||||
}
|
||||
|
||||
static let dicPhonabet2ASCII: [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",
|
||||
]
|
||||
|
||||
/// 內部函式,用以將被加密的注音讀音索引鍵進行解密。
|
||||
///
|
||||
/// 如果傳入的字串當中包含 ASCII 下畫線符號的話,則表明該字串並非注音讀音字串,會被忽略處理。
|
||||
/// - parameters:
|
||||
/// - incoming: 傳入的已加密注音讀音字串。
|
||||
static 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
|
||||
}
|
||||
|
||||
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": "˙",
|
||||
]
|
||||
}
|
||||
|
||||
public extension LMAssembly.LMInstantiator {
|
||||
@discardableResult static func connectToTestSQLDB() -> Bool {
|
||||
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":"ㄓㄨㄤ"}
|
||||
}
|
||||
"""#
|
|
@ -7,11 +7,13 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import Megrez
|
||||
import PinyinPhonaConverter
|
||||
import Shared
|
||||
|
||||
extension LMAssembly {
|
||||
struct LMAssociates {
|
||||
public extension vChewingLM {
|
||||
@frozen struct LMAssociates {
|
||||
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 = ""
|
||||
|
||||
public var count: Int { rangeMap.count }
|
||||
|
@ -46,8 +48,8 @@ extension LMAssembly {
|
|||
replaceData(textData: rawStrData)
|
||||
} catch {
|
||||
filePath = oldPath
|
||||
vCLMLog("\(error)")
|
||||
vCLMLog("↑ Exception happened when reading data at: \(path).")
|
||||
vCLog("\(error)")
|
||||
vCLog("↑ Exception happened when reading data at: \(path).")
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -91,21 +93,28 @@ extension LMAssembly {
|
|||
do {
|
||||
try strData.write(toFile: filePath, atomically: true, encoding: .utf8)
|
||||
} catch {
|
||||
vCLMLog("Failed to save current database to: \(filePath)")
|
||||
vCLog("Failed to save current database to: \(filePath)")
|
||||
}
|
||||
}
|
||||
|
||||
public func valuesFor(pair: Megrez.KeyValuePaired) -> [String] {
|
||||
var pairs: [String] = []
|
||||
let availableResults = [rangeMap[pair.toNGramKey], rangeMap[pair.value]].compactMap { $0 }
|
||||
availableResults.forEach { arrRangeRecords in
|
||||
arrRangeRecords.forEach { netaRange, index in
|
||||
if let arrRangeRecords: [(Range<String.Index>, Int)] = rangeMap[pair.toNGramKey] {
|
||||
for (netaRange, index) in arrRangeRecords {
|
||||
let neta = strData[netaRange].split(separator: " ")
|
||||
let theValue: String = .init(neta[index])
|
||||
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 {
|
||||
|
@ -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 LineReader
|
||||
import Megrez
|
||||
import Shared
|
||||
|
||||
extension LMAssembly {
|
||||
public extension vChewingLM {
|
||||
/// 磁帶模組,用來方便使用者自行擴充字根輸入法。
|
||||
struct LMCassette {
|
||||
@frozen struct LMCassette {
|
||||
public private(set) var filePath: String?
|
||||
public private(set) var nameShort: String = ""
|
||||
public private(set) var nameENG: String = ""
|
||||
|
@ -25,7 +26,6 @@ extension LMAssembly {
|
|||
public private(set) var selectionKeys: String = ""
|
||||
public private(set) var endKeys: [String] = []
|
||||
public private(set) var wildcardKey: String = ""
|
||||
public private(set) var keysToDirectlyCommit: String = ""
|
||||
public private(set) var keyNameMap: [String: String] = [:]
|
||||
public private(set) var quickDefMap: [String: String] = [:]
|
||||
public private(set) var charDefMap: [String: [String]] = [:]
|
||||
|
@ -39,278 +39,311 @@ extension LMAssembly {
|
|||
public private(set) var areCandidateKeysShiftHeld: Bool = false
|
||||
public private(set) var supplyQuickResults: Bool = false
|
||||
public private(set) var supplyPartiallyMatchedResults: Bool = false
|
||||
public var candidateKeysValidator: (String) -> Bool = { _ in false }
|
||||
/// 計算頻率時要用到的東西 - NORM
|
||||
|
||||
/// 計算頻率時要用到的東西
|
||||
private static let fscale = 2.7
|
||||
private var norm = 0.0
|
||||
}
|
||||
}
|
||||
|
||||
extension LMAssembly.LMCassette {
|
||||
/// 計算頻率時要用到的東西 - fscale
|
||||
private static let fscale = 2.7
|
||||
/// 萬用花牌字符,哪怕花牌鍵仍不可用。
|
||||
var wildcard: String { wildcardKey.isEmpty ? "†" : wildcardKey }
|
||||
/// 資料陣列內承載的核心 charDef 資料筆數。
|
||||
var count: Int { charDefMap.count }
|
||||
/// 是否已有資料載入。
|
||||
var isLoaded: Bool { !charDefMap.isEmpty }
|
||||
/// 返回「允許使用的敲字鍵」的陣列。
|
||||
var allowedKeys: [String] { Array(keyNameMap.keys + [" "]).deduplicated }
|
||||
/// 將給定的按鍵字母轉換成要顯示的形態。
|
||||
func convertKeyToDisplay(char: String) -> String {
|
||||
keyNameMap[char] ?? char
|
||||
}
|
||||
/// 萬用花牌字符,哪怕花牌鍵仍不可用。
|
||||
public var wildcard: String { wildcardKey.isEmpty ? "†" : wildcardKey }
|
||||
/// 資料陣列內承載的核心 charDef 資料筆數。
|
||||
public var count: Int { charDefMap.count }
|
||||
/// 是否已有資料載入。
|
||||
public var isLoaded: Bool { !charDefMap.isEmpty }
|
||||
/// 返回「允許使用的敲字鍵」的陣列。
|
||||
public var allowedKeys: [String] { Array(keyNameMap.keys + [" "]).deduplicated }
|
||||
/// 將給定的按鍵字母轉換成要顯示的形態。
|
||||
public func convertKeyToDisplay(char: String) -> String {
|
||||
keyNameMap[char] ?? char
|
||||
}
|
||||
|
||||
/// 載入給定的 CIN 檔案內容。
|
||||
/// - Note:
|
||||
/// - 檢查是否以 `%gen_inp` 或者 `%ename` 開頭、以確認其是否為 cin 檔案。在讀到這些資訊之前的行都會被忽略。
|
||||
/// - `%ename` 決定磁帶的英文名、`%cname` 決定磁帶的 CJK 名稱、
|
||||
/// `%sname` 決定磁帶的最短英文縮寫名稱、`%intlname` 決定磁帶的本地化名稱綜合字串。
|
||||
/// - `%encoding` 不處理,因為 Swift 只認 UTF-8。
|
||||
/// - `%selkey` 不處理,因為威注音輸入法有自己的選字鍵體系。
|
||||
/// - `%endkey` 是會觸發組字事件的按鍵。
|
||||
/// - `%wildcardkey` 決定磁帶的萬能鍵名稱,只有第一個字元會生效。
|
||||
/// - `%nullcandidate` 用來指明 `%quick` 字段給出的候選字當中有哪一種是無效的。
|
||||
/// - `%keyname begin` 至 `%keyname end` 之間是字根翻譯表,先讀取為 Swift 辭典以備用。
|
||||
/// - `%quick begin` 至 `%quick end` 之間則是簡碼資料,對應的 value 得拆成單個漢字。
|
||||
/// - `%chardef begin` 至 `%chardef end` 之間則是詞庫資料。
|
||||
/// - `%symboldef begin` 至 `%symboldef end` 之間則是符號選單的專用資料。
|
||||
/// - `%octagram begin` 至 `%octagram end` 之間則是詞語頻次資料。
|
||||
/// 第三欄資料為對應字根、可有可無。第一欄與第二欄分別為「字詞」與「統計頻次」。
|
||||
/// - Parameter path: 檔案路徑。
|
||||
/// - Returns: 是否載入成功。
|
||||
@discardableResult mutating func open(_ path: String) -> Bool {
|
||||
if isLoaded { return false }
|
||||
let oldPath = filePath
|
||||
filePath = nil
|
||||
if FileManager.default.fileExists(atPath: path) {
|
||||
do {
|
||||
guard let fileHandle = FileHandle(forReadingAtPath: path) else {
|
||||
throw LMAssembly.FileErrors.fileHandleError("")
|
||||
}
|
||||
let lineReader = try LineReader(file: fileHandle)
|
||||
var theMaxKeyLength = 1
|
||||
var loadingKeys = false
|
||||
var loadingQuickSets = false {
|
||||
willSet {
|
||||
supplyQuickResults = true
|
||||
if !newValue, quickDefMap.keys.contains(wildcardKey) { wildcardKey = "" }
|
||||
/// 載入給定的 CIN 檔案內容。
|
||||
/// - Note:
|
||||
/// - 檢查是否以 `%gen_inp` 或者 `%ename` 開頭、以確認其是否為 cin 檔案。在讀到這些資訊之前的行都會被忽略。
|
||||
/// - `%ename` 決定磁帶的英文名、`%cname` 決定磁帶的 CJK 名稱、
|
||||
/// `%sname` 決定磁帶的最短英文縮寫名稱、`%intlname` 決定磁帶的本地化名稱綜合字串。
|
||||
/// - `%encoding` 不處理,因為 Swift 只認 UTF-8。
|
||||
/// - `%selkey` 不處理,因為威注音輸入法有自己的選字鍵體系。
|
||||
/// - `%endkey` 是會觸發組字事件的按鍵。
|
||||
/// - `%wildcardkey` 決定磁帶的萬能鍵名稱,只有第一個字元會生效。
|
||||
/// - `%nullcandidate` 用來指明 `%quick` 字段給出的候選字當中有哪一種是無效的。
|
||||
/// - `%keyname begin` 至 `%keyname end` 之間是字根翻譯表,先讀取為 Swift 辭典以備用。
|
||||
/// - `%quick begin` 至 `%quick end` 之間則是簡碼資料,對應的 value 得拆成單個漢字。
|
||||
/// - `%chardef begin` 至 `%chardef end` 之間則是詞庫資料。
|
||||
/// - `%symboldef begin` 至 `%symboldef end` 之間則是符號選單的專用資料。
|
||||
/// - `%octagram begin` 至 `%octagram end` 之間則是詞語頻次資料。
|
||||
/// 第三欄資料為對應字根、可有可無。第一欄與第二欄分別為「字詞」與「統計頻次」。
|
||||
/// - Parameter path: 檔案路徑。
|
||||
/// - Returns: 是否載入成功。
|
||||
@discardableResult public mutating func open(_ path: String) -> Bool {
|
||||
if isLoaded { return false }
|
||||
let oldPath = filePath
|
||||
filePath = nil
|
||||
if FileManager.default.fileExists(atPath: path) {
|
||||
do {
|
||||
guard let fileHandle = FileHandle(forReadingAtPath: path) else {
|
||||
throw FileErrors.fileHandleError("")
|
||||
}
|
||||
}
|
||||
var loadingCharDefinitions = false {
|
||||
willSet {
|
||||
if !newValue, charDefMap.keys.contains(wildcardKey) { wildcardKey = "" }
|
||||
}
|
||||
}
|
||||
var loadingSymbolDefinitions = false {
|
||||
willSet {
|
||||
if !newValue, symbolDefMap.keys.contains(wildcardKey) { wildcardKey = "" }
|
||||
}
|
||||
}
|
||||
var loadingOctagramData = false
|
||||
var keysUsedInCharDef: Set<String> = .init()
|
||||
|
||||
for strLine in lineReader {
|
||||
let isTabDelimiting = strLine.contains("\t")
|
||||
let cells = isTabDelimiting ? strLine.split(separator: "\t") : strLine.split(separator: " ")
|
||||
guard cells.count >= 1 else { continue }
|
||||
let strFirstCell = cells[0].trimmingCharacters(in: .newlines)
|
||||
let strSecondCell = cells.count >= 2 ? cells[1].trimmingCharacters(in: .newlines) : nil
|
||||
// 處理雜項資訊
|
||||
if strLine.first == "%", strFirstCell != "%" {
|
||||
let lineReader = try LineReader(file: fileHandle)
|
||||
var theMaxKeyLength = 1
|
||||
var loadingKeys = false
|
||||
var loadingQuickSets = false
|
||||
var loadingCharDefinitions = false
|
||||
var loadingSymbolDefinitions = false
|
||||
var loadingOctagramData = false
|
||||
var keysUsedInCharDef: Set<String> = .init()
|
||||
for strLine in lineReader {
|
||||
if strLine.starts(with: "%keyname") {
|
||||
if !loadingKeys, strLine.contains("begin") { loadingKeys = true }
|
||||
if loadingKeys, strLine.contains("end") { loadingKeys = false }
|
||||
}
|
||||
// %flag_disp_partial_match
|
||||
if strLine == "%flag_disp_partial_match" {
|
||||
supplyPartiallyMatchedResults = true
|
||||
supplyQuickResults = true
|
||||
}
|
||||
guard let strSecondCell = strSecondCell else { continue }
|
||||
processTags: switch strFirstCell {
|
||||
case "%keyname" where strSecondCell == "begin": loadingKeys = true
|
||||
case "%keyname" where strSecondCell == "end": loadingKeys = false
|
||||
case "%quick" where strSecondCell == "begin": loadingQuickSets = true
|
||||
case "%quick" where strSecondCell == "end": loadingQuickSets = false
|
||||
case "%chardef" where strSecondCell == "begin": loadingCharDefinitions = true
|
||||
case "%chardef" where strSecondCell == "end": loadingCharDefinitions = false
|
||||
case "%symboldef" where strSecondCell == "begin": loadingSymbolDefinitions = true
|
||||
case "%symboldef" where strSecondCell == "end": loadingSymbolDefinitions = false
|
||||
case "%octagram" where strSecondCell == "begin": loadingOctagramData = true
|
||||
case "%octagram" where strSecondCell == "end": loadingOctagramData = false
|
||||
case "%ename" where nameENG.isEmpty:
|
||||
parseSubCells: for neta in strSecondCell.components(separatedBy: ";") {
|
||||
// %quick
|
||||
if strLine.starts(with: "%quick") {
|
||||
supplyQuickResults = true
|
||||
if !loadingQuickSets, strLine.contains("begin") {
|
||||
loadingQuickSets = true
|
||||
}
|
||||
if loadingQuickSets, strLine.contains("end") {
|
||||
loadingQuickSets = false
|
||||
if quickDefMap.keys.contains(wildcardKey) { wildcardKey = "" }
|
||||
}
|
||||
}
|
||||
// %chardef
|
||||
if strLine.starts(with: "%chardef") {
|
||||
if !loadingCharDefinitions, strLine.contains("begin") {
|
||||
loadingCharDefinitions = true
|
||||
}
|
||||
if loadingCharDefinitions, strLine.contains("end") {
|
||||
loadingCharDefinitions = false
|
||||
if charDefMap.keys.contains(wildcardKey) { wildcardKey = "" }
|
||||
}
|
||||
}
|
||||
// %symboldef
|
||||
if strLine.starts(with: "%symboldef") {
|
||||
if !loadingSymbolDefinitions, strLine.contains("begin") {
|
||||
loadingSymbolDefinitions = true
|
||||
}
|
||||
if loadingSymbolDefinitions, strLine.contains("end") {
|
||||
loadingSymbolDefinitions = false
|
||||
if symbolDefMap.keys.contains(wildcardKey) { wildcardKey = "" }
|
||||
}
|
||||
}
|
||||
// %octagram
|
||||
if strLine.starts(with: "%octagram") {
|
||||
if !loadingOctagramData, strLine.contains("begin") {
|
||||
loadingOctagramData = true
|
||||
}
|
||||
if loadingOctagramData, strLine.contains("end") {
|
||||
loadingOctagramData = false
|
||||
}
|
||||
}
|
||||
// Start data parsing.
|
||||
let cells: [String.SubSequence] =
|
||||
strLine.contains("\t") ? strLine.split(separator: "\t") : strLine.split(separator: " ")
|
||||
guard cells.count >= 2 else { continue }
|
||||
let strFirstCell = cells[0].trimmingCharacters(in: .newlines)
|
||||
let strSecondCell = cells[1].trimmingCharacters(in: .newlines)
|
||||
if loadingKeys, !cells[0].starts(with: "%keyname") {
|
||||
keyNameMap[strFirstCell] = cells[1].trimmingCharacters(in: .newlines)
|
||||
} else if loadingQuickSets, !strLine.starts(with: "%quick") {
|
||||
theMaxKeyLength = max(theMaxKeyLength, cells[0].count)
|
||||
quickDefMap[strFirstCell, default: .init()].append(strSecondCell)
|
||||
} else if loadingCharDefinitions, !loadingSymbolDefinitions,
|
||||
!strLine.starts(with: "%chardef"), !strLine.starts(with: "%symboldef")
|
||||
{
|
||||
theMaxKeyLength = max(theMaxKeyLength, cells[0].count)
|
||||
charDefMap[strFirstCell, default: []].append(strSecondCell)
|
||||
if strFirstCell.count > 1 {
|
||||
strFirstCell.map(\.description).forEach { keyChar in
|
||||
keysUsedInCharDef.insert(keyChar.description)
|
||||
}
|
||||
}
|
||||
reverseLookupMap[strSecondCell, default: []].append(strFirstCell)
|
||||
var keyComps = strFirstCell.map(\.description)
|
||||
while !keyComps.isEmpty {
|
||||
keyComps.removeLast()
|
||||
charDefWildcardMap[keyComps.joined() + wildcard, default: []].append(strSecondCell)
|
||||
}
|
||||
} else if loadingSymbolDefinitions, !strLine.starts(with: "%chardef"), !strLine.starts(with: "%symboldef") {
|
||||
theMaxKeyLength = max(theMaxKeyLength, cells[0].count)
|
||||
symbolDefMap[strFirstCell, default: []].append(strSecondCell)
|
||||
reverseLookupMap[strSecondCell, default: []].append(strFirstCell)
|
||||
} else if loadingOctagramData, !strLine.starts(with: "%octagram") {
|
||||
guard let countValue = Int(cells[1]) else { continue }
|
||||
switch cells.count {
|
||||
case 2: octagramMap[strFirstCell] = countValue
|
||||
case 3: octagramDividedMap[strFirstCell] = (countValue, cells[2].trimmingCharacters(in: .newlines))
|
||||
default: break
|
||||
}
|
||||
norm += Self.fscale ** (Double(cells[0].count) / 3.0 - 1.0) * Double(countValue)
|
||||
}
|
||||
guard !loadingKeys, !loadingQuickSets, !loadingCharDefinitions, !loadingOctagramData else { continue }
|
||||
if nameENG.isEmpty, strLine.starts(with: "%ename ") {
|
||||
for neta in cells[1].components(separatedBy: ";") {
|
||||
let subNetaGroup = neta.components(separatedBy: ":")
|
||||
guard subNetaGroup.count == 2, subNetaGroup[1].contains("en") else { continue }
|
||||
nameENG = String(subNetaGroup[0])
|
||||
break parseSubCells
|
||||
if subNetaGroup.count == 2, subNetaGroup[1].contains("en") {
|
||||
nameENG = String(subNetaGroup[0])
|
||||
break
|
||||
}
|
||||
}
|
||||
guard nameENG.isEmpty else { break processTags }
|
||||
nameENG = strSecondCell
|
||||
case "%intlname" where nameIntl.isEmpty: nameIntl = strSecondCell.replacingOccurrences(of: "_", with: " ")
|
||||
case "%cname" where nameCJK.isEmpty: nameCJK = strSecondCell
|
||||
case "%sname" where nameShort.isEmpty: nameShort = strSecondCell
|
||||
case "%nullcandidate" where nullCandidate.isEmpty: nullCandidate = strSecondCell
|
||||
case "%selkey" where selectionKeys.isEmpty: selectionKeys = strSecondCell.map(\.description).deduplicated.joined()
|
||||
case "%endkey" where endKeys.isEmpty: endKeys = strSecondCell.map(\.description).deduplicated
|
||||
case "%wildcardkey" where wildcardKey.isEmpty: wildcardKey = strSecondCell.first?.description ?? ""
|
||||
case "%keys_to_directly_commit" where keysToDirectlyCommit.isEmpty: keysToDirectlyCommit = strSecondCell
|
||||
default: break processTags
|
||||
if nameENG.isEmpty { nameENG = strSecondCell }
|
||||
}
|
||||
if nameIntl.isEmpty, strLine.starts(with: "%intlname ") {
|
||||
nameIntl = strSecondCell.replacingOccurrences(of: "_", with: " ")
|
||||
}
|
||||
if nameCJK.isEmpty, strLine.starts(with: "%cname ") { nameCJK = strSecondCell }
|
||||
if nameShort.isEmpty, strLine.starts(with: "%sname ") { nameShort = strSecondCell }
|
||||
if nullCandidate.isEmpty, strLine.starts(with: "%nullcandidate ") { nullCandidate = strSecondCell }
|
||||
if selectionKeys.isEmpty, strLine.starts(with: "%selkey ") {
|
||||
selectionKeys = cells[1].map(\.description).deduplicated.joined()
|
||||
}
|
||||
if endKeys.isEmpty, strLine.starts(with: "%endkey ") {
|
||||
endKeys = cells[1].map(\.description).deduplicated
|
||||
}
|
||||
if wildcardKey.isEmpty, strLine.starts(with: "%wildcardkey ") {
|
||||
wildcardKey = cells[1].first?.description ?? ""
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 處理普通資料
|
||||
guard let strSecondCell = strSecondCell else { continue }
|
||||
if loadingKeys {
|
||||
keyNameMap[strFirstCell] = strSecondCell.trimmingCharacters(in: .newlines)
|
||||
} else if loadingQuickSets {
|
||||
theMaxKeyLength = max(theMaxKeyLength, cells[0].count)
|
||||
quickDefMap[strFirstCell, default: .init()].append(strSecondCell)
|
||||
} else if loadingCharDefinitions, !loadingSymbolDefinitions {
|
||||
theMaxKeyLength = max(theMaxKeyLength, cells[0].count)
|
||||
charDefMap[strFirstCell, default: []].append(strSecondCell)
|
||||
if strFirstCell.count > 1 {
|
||||
strFirstCell.map(\.description).forEach { keyChar in
|
||||
keysUsedInCharDef.insert(keyChar.description)
|
||||
}
|
||||
}
|
||||
reverseLookupMap[strSecondCell, default: []].append(strFirstCell)
|
||||
var keyComps = strFirstCell.map(\.description)
|
||||
while !keyComps.isEmpty {
|
||||
keyComps.removeLast()
|
||||
charDefWildcardMap[keyComps.joined() + wildcard, default: []].append(strSecondCell)
|
||||
}
|
||||
} else if loadingSymbolDefinitions {
|
||||
theMaxKeyLength = max(theMaxKeyLength, cells[0].count)
|
||||
symbolDefMap[strFirstCell, default: []].append(strSecondCell)
|
||||
reverseLookupMap[strSecondCell, default: []].append(strFirstCell)
|
||||
} else if loadingOctagramData {
|
||||
guard let countValue = Int(strSecondCell) else { continue }
|
||||
switch cells.count {
|
||||
case 2: octagramMap[strFirstCell] = countValue
|
||||
case 3: octagramDividedMap[strFirstCell] = (countValue, cells[2].trimmingCharacters(in: .newlines))
|
||||
default: break
|
||||
}
|
||||
norm += Self.fscale ** (Double(cells[0].count) / 3.0 - 1.0) * Double(countValue)
|
||||
// Post process.
|
||||
if CandidateKey.validate(keys: selectionKeys) != nil { selectionKeys = "1234567890" }
|
||||
if !keysUsedInCharDef.intersection(selectionKeys.map(\.description)).isEmpty {
|
||||
areCandidateKeysShiftHeld = true
|
||||
}
|
||||
maxKeyLength = theMaxKeyLength
|
||||
keyNameMap[wildcardKey] = keyNameMap[wildcardKey] ?? "?"
|
||||
filePath = path
|
||||
return true
|
||||
} catch {
|
||||
vCLog("CIN Loading Failed: File Access Error.")
|
||||
}
|
||||
// Post process.
|
||||
// 備註:因為 Package 層級嵌套的現狀,此處不太方便檢查是否需要篩掉 J / K 鍵。
|
||||
// 因此只能在其他地方做篩檢。
|
||||
if !candidateKeysValidator(selectionKeys) { selectionKeys = "1234567890" }
|
||||
if !keysUsedInCharDef.intersection(selectionKeys.map(\.description)).isEmpty {
|
||||
areCandidateKeysShiftHeld = true
|
||||
}
|
||||
maxKeyLength = theMaxKeyLength
|
||||
keyNameMap[wildcardKey] = keyNameMap[wildcardKey] ?? "?"
|
||||
filePath = path
|
||||
return true
|
||||
} catch {
|
||||
vCLMLog("CIN Loading Failed: File Access Error.")
|
||||
}
|
||||
} else {
|
||||
vCLMLog("CIN Loading Failed: File Missing.")
|
||||
}
|
||||
filePath = oldPath
|
||||
return false
|
||||
}
|
||||
|
||||
mutating func clear() {
|
||||
self = .init()
|
||||
}
|
||||
|
||||
func quickSetsFor(key: String) -> String? {
|
||||
guard !key.isEmpty else { return nil }
|
||||
var result = [String]()
|
||||
if let specifiedResult = quickDefMap[key], !specifiedResult.isEmpty {
|
||||
result.append(contentsOf: specifiedResult.map(\.description))
|
||||
}
|
||||
if supplyQuickResults, result.isEmpty {
|
||||
if supplyPartiallyMatchedResults {
|
||||
let fetched = charDefMap.compactMap {
|
||||
$0.key.starts(with: key) ? $0 : nil
|
||||
}.stableSort {
|
||||
$0.key.count < $1.key.count
|
||||
}.flatMap(\.value).filter {
|
||||
$0.count == 1
|
||||
}
|
||||
result.append(contentsOf: fetched.deduplicated.prefix(selectionKeys.count * 6))
|
||||
} else {
|
||||
let fetched = (charDefMap[key] ?? [String]()).filter { $0.count == 1 }
|
||||
result.append(contentsOf: fetched.deduplicated.prefix(selectionKeys.count * 6))
|
||||
vCLog("CIN Loading Failed: File Missing.")
|
||||
}
|
||||
filePath = oldPath
|
||||
return false
|
||||
}
|
||||
return result.isEmpty ? nil : result.joined(separator: "\t")
|
||||
}
|
||||
|
||||
/// 根據給定的字根索引鍵,來獲取資料庫辭典內的對應結果。
|
||||
/// - parameters:
|
||||
/// - key: 讀音索引鍵。
|
||||
func unigramsFor(key: String) -> [Megrez.Unigram] {
|
||||
let arrRaw = charDefMap[key]?.deduplicated ?? []
|
||||
var arrRawWildcard: [String] = []
|
||||
if let arrRawWildcardValues = charDefWildcardMap[key]?.deduplicated,
|
||||
key.contains(wildcard), key.first?.description != wildcard
|
||||
{
|
||||
arrRawWildcard.append(contentsOf: arrRawWildcardValues)
|
||||
public mutating func clear() {
|
||||
filePath = nil
|
||||
nullCandidate.removeAll()
|
||||
keyNameMap.removeAll()
|
||||
quickDefMap.removeAll()
|
||||
charDefMap.removeAll()
|
||||
charDefWildcardMap.removeAll()
|
||||
nameShort.removeAll()
|
||||
nameENG.removeAll()
|
||||
nameCJK.removeAll()
|
||||
selectionKeys.removeAll()
|
||||
endKeys.removeAll()
|
||||
reverseLookupMap.removeAll()
|
||||
octagramMap.removeAll()
|
||||
octagramDividedMap.removeAll()
|
||||
wildcardKey.removeAll()
|
||||
nameIntl.removeAll()
|
||||
maxKeyLength = 1
|
||||
norm = 0
|
||||
}
|
||||
var arrResults = [Megrez.Unigram]()
|
||||
var lowestScore: Double = 0
|
||||
for neta in arrRaw {
|
||||
let theScore: Double = {
|
||||
if let freqDataPair = octagramDividedMap[neta], key == freqDataPair.1 {
|
||||
return calculateWeight(count: freqDataPair.0, phraseLength: neta.count)
|
||||
} else if let freqData = octagramMap[neta] {
|
||||
return calculateWeight(count: freqData, phraseLength: neta.count)
|
||||
|
||||
public func quickSetsFor(key: String) -> String? {
|
||||
guard !key.isEmpty else { return nil }
|
||||
var result = [String]()
|
||||
if let specifiedResult = quickDefMap[key], !specifiedResult.isEmpty {
|
||||
result.append(contentsOf: specifiedResult.map(\.description))
|
||||
}
|
||||
if supplyQuickResults, result.isEmpty {
|
||||
if supplyPartiallyMatchedResults {
|
||||
let fetched = charDefMap.compactMap {
|
||||
$0.key.starts(with: key) ? $0 : nil
|
||||
}.stableSort {
|
||||
$0.key.count < $1.key.count
|
||||
}.flatMap(\.value).filter {
|
||||
$0.count == 1
|
||||
}
|
||||
result.append(contentsOf: fetched.deduplicated.prefix(selectionKeys.count * 6))
|
||||
} else {
|
||||
let fetched = (charDefMap[key] ?? [String]()).filter { $0.count == 1 }
|
||||
result.append(contentsOf: fetched.deduplicated.prefix(selectionKeys.count * 6))
|
||||
}
|
||||
return Double(arrResults.count) * -0.001 - 9.5
|
||||
}()
|
||||
lowestScore = min(theScore, lowestScore)
|
||||
arrResults.append(.init(value: neta, score: theScore))
|
||||
}
|
||||
return result.isEmpty ? nil : result.joined(separator: "\t")
|
||||
}
|
||||
lowestScore = min(-9.5, lowestScore)
|
||||
if !arrRawWildcard.isEmpty {
|
||||
for neta in arrRawWildcard {
|
||||
var theScore: Double = {
|
||||
|
||||
/// 根據給定的字根索引鍵,來獲取資料庫辭典內的對應結果。
|
||||
/// - parameters:
|
||||
/// - key: 讀音索引鍵。
|
||||
public func unigramsFor(key: String) -> [Megrez.Unigram] {
|
||||
let arrRaw = charDefMap[key]?.deduplicated ?? []
|
||||
var arrRawWildcard: [String] = []
|
||||
if let arrRawWildcardValues = charDefWildcardMap[key]?.deduplicated,
|
||||
key.contains(wildcard), key.first?.description != wildcard
|
||||
{
|
||||
arrRawWildcard.append(contentsOf: arrRawWildcardValues)
|
||||
}
|
||||
var arrResults = [Megrez.Unigram]()
|
||||
var lowestScore: Double = 0
|
||||
for neta in arrRaw {
|
||||
let theScore: Double = {
|
||||
if let freqDataPair = octagramDividedMap[neta], key == freqDataPair.1 {
|
||||
return calculateWeight(count: freqDataPair.0, phraseLength: neta.count)
|
||||
} else if let freqData = octagramMap[neta] {
|
||||
return calculateWeight(count: freqData, phraseLength: neta.count)
|
||||
}
|
||||
return Double(arrResults.count) * -0.001 - 9.7
|
||||
return Double(arrResults.count) * -0.001 - 9.5
|
||||
}()
|
||||
theScore += lowestScore
|
||||
lowestScore = min(theScore, lowestScore)
|
||||
arrResults.append(.init(value: neta, score: theScore))
|
||||
}
|
||||
lowestScore = min(-9.5, lowestScore)
|
||||
if !arrRawWildcard.isEmpty {
|
||||
for neta in arrRawWildcard {
|
||||
var theScore: Double = {
|
||||
if let freqDataPair = octagramDividedMap[neta], key == freqDataPair.1 {
|
||||
return calculateWeight(count: freqDataPair.0, phraseLength: neta.count)
|
||||
} else if let freqData = octagramMap[neta] {
|
||||
return calculateWeight(count: freqData, phraseLength: neta.count)
|
||||
}
|
||||
return Double(arrResults.count) * -0.001 - 9.7
|
||||
}()
|
||||
theScore += lowestScore
|
||||
arrResults.append(.init(value: neta, score: theScore))
|
||||
}
|
||||
}
|
||||
return arrResults
|
||||
}
|
||||
return arrResults
|
||||
}
|
||||
|
||||
/// 根據給定的讀音索引鍵來確認資料庫辭典內是否存在對應的資料。
|
||||
/// - parameters:
|
||||
/// - key: 讀音索引鍵。
|
||||
func hasUnigramsFor(key: String) -> Bool {
|
||||
charDefMap[key] != nil
|
||||
|| (charDefWildcardMap[key] != nil && key.contains(wildcard) && key.first?.description != wildcard)
|
||||
}
|
||||
|
||||
// MARK: - Private Functions.
|
||||
|
||||
private func calculateWeight(count theCount: Int, phraseLength: Int) -> Double {
|
||||
var weight: Double = 0
|
||||
switch theCount {
|
||||
case -2: // 拗音假名
|
||||
weight = -13
|
||||
case -1: // 單個假名
|
||||
weight = -13
|
||||
case 0: // 墊底低頻漢字與詞語
|
||||
weight = log10(
|
||||
Self.fscale ** (Double(phraseLength) / 3.0 - 1.0) * 0.25 / norm)
|
||||
default:
|
||||
weight = log10(
|
||||
Self.fscale ** (Double(phraseLength) / 3.0 - 1.0)
|
||||
* Double(theCount) / norm
|
||||
)
|
||||
/// 根據給定的讀音索引鍵來確認資料庫辭典內是否存在對應的資料。
|
||||
/// - parameters:
|
||||
/// - key: 讀音索引鍵。
|
||||
public func hasUnigramsFor(key: String) -> Bool {
|
||||
charDefMap[key] != nil
|
||||
|| (charDefWildcardMap[key] != nil && key.contains(wildcard) && key.first?.description != wildcard)
|
||||
}
|
||||
|
||||
// MARK: - Private Functions.
|
||||
|
||||
private func calculateWeight(count theCount: Int, phraseLength: Int) -> Double {
|
||||
var weight: Double = 0
|
||||
switch theCount {
|
||||
case -2: // 拗音假名
|
||||
weight = -13
|
||||
case -1: // 單個假名
|
||||
weight = -13
|
||||
case 0: // 墊底低頻漢字與詞語
|
||||
weight = log10(
|
||||
Self.fscale ** (Double(phraseLength) / 3.0 - 1.0) * 0.25 / norm)
|
||||
default:
|
||||
weight = log10(
|
||||
Self.fscale ** (Double(phraseLength) / 3.0 - 1.0)
|
||||
* Double(theCount) / norm
|
||||
)
|
||||
}
|
||||
return weight
|
||||
}
|
||||
return weight
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,13 +7,15 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import Megrez
|
||||
import PinyinPhonaConverter
|
||||
import Shared
|
||||
|
||||
extension LMAssembly {
|
||||
public extension vChewingLM {
|
||||
/// 與之前的 LMCore 不同,LMCoreEX 不在辭典內記錄實體,而是記錄 range 範圍。
|
||||
/// 需要資料的時候,直接拿 range 去 strData 取資料。
|
||||
/// 資料記錄原理與上游 C++ 的 ParselessLM 差不多,但用的是 Swift 原生手段。
|
||||
/// 主要時間消耗仍在 For 迴圈,但這個算法可以顯著減少記憶體佔用。
|
||||
struct LMCoreEX {
|
||||
@frozen struct LMCoreEX {
|
||||
public private(set) var filePath: String?
|
||||
/// 資料庫辭典。索引內容為注音字串,資料內容則為字串首尾範圍、方便自 strData 取資料。
|
||||
var rangeMap: [String: [Range<String.Index>]] = [:]
|
||||
|
@ -79,8 +81,8 @@ extension LMAssembly {
|
|||
replaceData(textData: rawStrData)
|
||||
} catch {
|
||||
filePath = oldPath
|
||||
vCLMLog("\(error)")
|
||||
vCLMLog("↑ Exception happened when reading data at: \(path).")
|
||||
vCLog("\(error)")
|
||||
vCLog("↑ Exception happened when reading data at: \(path).")
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -131,7 +133,7 @@ extension LMAssembly {
|
|||
}
|
||||
try dataToWrite.write(toFile: filePath, atomically: true, encoding: .utf8)
|
||||
} 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
|
||||
}
|
||||
}
|
||||
vCLMLog(strDump)
|
||||
vCLog(strDump)
|
||||
}
|
||||
|
||||
/// 根據給定的讀音索引鍵,來獲取資料庫辭典內的對應資料陣列的字串首尾範圍資料、據此自 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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,236 @@
|
|||
// (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
|
||||
import Shared
|
||||
|
||||
public extension vChewingLM {
|
||||
/// 與之前的 LMCore 不同,LMCoreJSON 直接讀取 json。
|
||||
/// 這樣一來可以節省在舊 mac 機種內的資料讀入速度。
|
||||
/// 目前僅針對輸入法原廠語彙資料檔案使用 json 格式。
|
||||
@frozen struct LMCoreJSON {
|
||||
public private(set) var filePath: String?
|
||||
/// 資料庫辭典。索引內容為經過加密的注音字串,資料內容則為 UTF8 資料陣列。
|
||||
var dataMap: [String: [String]] = [:]
|
||||
/// 【已作廢】資料庫字串陣列。在 LMCoreJSON 內沒有作用。
|
||||
var strData: String = ""
|
||||
/// 【已作廢】聲明原始檔案內第一、二縱列的內容是否彼此顛倒。
|
||||
var shouldReverse = false
|
||||
/// 請且僅請對使用者語言模組啟用該參數:是否自動整理格式。
|
||||
var allowConsolidation = false
|
||||
/// 當某一筆資料內的權重資料毀損時,要施加的預設權重。
|
||||
var defaultScore: Double = 0
|
||||
/// 啟用該選項的話,會強制施加預設權重、而無視原始權重資料。
|
||||
var shouldForceDefaultScore = false
|
||||
|
||||
/// 資料陣列內承載的資料筆數。
|
||||
public var count: Int { dataMap.count }
|
||||
|
||||
/// 初期化該語言模型。
|
||||
///
|
||||
/// 某些參數在 LMCoreJSON 內已作廢,但仍保留、以方便那些想用該專案源碼做實驗的人群。
|
||||
///
|
||||
/// - parameters:
|
||||
/// - reverse: 已作廢:聲明原始檔案內第一、二縱列的內容是否彼此顛倒。
|
||||
/// - consolidate: 請且僅請對使用者語言模組啟用該參數:是否自動整理格式。
|
||||
/// - defaultScore: 當某一筆資料內的權重資料毀損時,要施加的預設權重。
|
||||
/// - forceDefaultScore: 啟用該選項的話,會強制施加預設權重、而無視原始權重資料。
|
||||
public init(
|
||||
reverse: Bool = false, consolidate: Bool = false, defaultScore scoreDefault: Double = 0,
|
||||
forceDefaultScore: Bool = false
|
||||
) {
|
||||
dataMap = [:]
|
||||
allowConsolidation = consolidate
|
||||
shouldReverse = reverse
|
||||
defaultScore = scoreDefault
|
||||
shouldForceDefaultScore = forceDefaultScore
|
||||
}
|
||||
|
||||
/// 檢測資料庫辭典內是否已經有載入的資料。
|
||||
public var isLoaded: Bool { !dataMap.isEmpty }
|
||||
|
||||
/// 讀入資料辭典。
|
||||
/// - parameters:
|
||||
/// - dictData: 辭典資料及對應的 URL 位置。
|
||||
public mutating func load(_ dictData: (dict: [String: [String]], path: String)) {
|
||||
if isLoaded { return }
|
||||
filePath = dictData.path
|
||||
dataMap = dictData.dict
|
||||
}
|
||||
|
||||
/// 將資料從檔案讀入至資料庫辭典內。
|
||||
/// - parameters:
|
||||
/// - path: 給定路徑。
|
||||
@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("↑ Exception happened when reading JSON file at: \(path).")
|
||||
return false
|
||||
}
|
||||
|
||||
filePath = path
|
||||
return true
|
||||
}
|
||||
|
||||
/// 將當前語言模組的資料庫辭典自記憶體內卸除。
|
||||
public mutating func clear() {
|
||||
filePath = nil
|
||||
strData.removeAll()
|
||||
dataMap.removeAll()
|
||||
}
|
||||
|
||||
// MARK: - Advanced features
|
||||
|
||||
public func saveData() {
|
||||
guard let filePath = filePath, let jsonURL = URL(string: filePath) else { return }
|
||||
do {
|
||||
try JSONSerialization.data(withJSONObject: dataMap, options: .sortedKeys).write(to: jsonURL)
|
||||
} catch {
|
||||
vCLog("Failed to save current database to: \(filePath)")
|
||||
}
|
||||
}
|
||||
|
||||
/// 將當前資料庫辭典的內容以文本的形式輸出至 macOS 內建的 Console.app。
|
||||
///
|
||||
/// 該功能僅作偵錯之用途。
|
||||
public func dump() {
|
||||
var strDump = ""
|
||||
for entry in dataMap {
|
||||
let netaSets: [String] = entry.value
|
||||
let theKey = entry.key
|
||||
for strNetaSet in netaSets {
|
||||
let neta = Array(strNetaSet.trimmingCharacters(in: .newlines).components(separatedBy: " ").reversed())
|
||||
let theValue = neta[0]
|
||||
var theScore = defaultScore
|
||||
if neta.count >= 2, !shouldForceDefaultScore {
|
||||
theScore = .init(String(neta[1])) ?? defaultScore
|
||||
}
|
||||
strDump += "\(Self.cnvPhonabetToASCII(theKey)) \(theValue) \(theScore)\n"
|
||||
}
|
||||
}
|
||||
vCLog(strDump)
|
||||
}
|
||||
|
||||
public func getHaninSymbolMenuUnigrams() -> [Megrez.Unigram] {
|
||||
let key = "_punctuation_list"
|
||||
var grams: [Megrez.Unigram] = []
|
||||
guard let arrRangeRecords: [String] = dataMap[Self.cnvPhonabetToASCII(key)] else { return grams }
|
||||
for strNetaSet in arrRangeRecords {
|
||||
let neta = Array(strNetaSet.trimmingCharacters(in: .newlines).split(separator: " ").reversed())
|
||||
let theValue: String = .init(neta[0])
|
||||
var theScore = defaultScore
|
||||
if neta.count >= 2, !shouldForceDefaultScore {
|
||||
theScore = .init(String(neta[1])) ?? defaultScore
|
||||
}
|
||||
if theScore > 0 {
|
||||
theScore *= -1 // 應對可能忘記寫負號的情形
|
||||
}
|
||||
grams.append(Megrez.Unigram(value: theValue, score: theScore))
|
||||
}
|
||||
return grams
|
||||
}
|
||||
|
||||
/// 根據給定的讀音索引鍵,來獲取資料庫辭典內的對應資料陣列的 UTF8 資料、就地分析、生成單元圖陣列。
|
||||
/// - parameters:
|
||||
/// - key: 讀音索引鍵。
|
||||
public func unigramsFor(key: String) -> [Megrez.Unigram] {
|
||||
if key == "_punctuation_list" { return [] }
|
||||
var grams: [Megrez.Unigram] = []
|
||||
var gramsHW: [Megrez.Unigram] = []
|
||||
guard let arrRangeRecords: [String] = dataMap[Self.cnvPhonabetToASCII(key)] else { return grams }
|
||||
for strNetaSet in arrRangeRecords {
|
||||
let neta = Array(strNetaSet.trimmingCharacters(in: .newlines).split(separator: " ").reversed())
|
||||
let theValue: String = .init(neta[0])
|
||||
var theScore = defaultScore
|
||||
if neta.count >= 2, !shouldForceDefaultScore {
|
||||
theScore = .init(String(neta[1])) ?? defaultScore
|
||||
}
|
||||
if theScore > 0 {
|
||||
theScore *= -1 // 應對可能忘記寫負號的情形
|
||||
}
|
||||
grams.append(Megrez.Unigram(value: theValue, score: theScore))
|
||||
if !key.contains("_punctuation") { continue }
|
||||
let halfValue = theValue.applyingTransformFW2HW(reverse: false)
|
||||
if halfValue != theValue {
|
||||
gramsHW.append(Megrez.Unigram(value: halfValue, score: theScore))
|
||||
}
|
||||
}
|
||||
grams.append(contentsOf: gramsHW)
|
||||
return grams
|
||||
}
|
||||
|
||||
/// 根據給定的讀音索引鍵來確認資料庫辭典內是否存在對應的資料。
|
||||
/// - parameters:
|
||||
/// - key: 讀音索引鍵。
|
||||
public func hasUnigramsFor(key: String) -> Bool {
|
||||
dataMap[Self.cnvPhonabetToASCII(key)] != nil
|
||||
}
|
||||
|
||||
/// 內部函式,用以將注音讀音索引鍵進行加密。
|
||||
///
|
||||
/// 使用這種加密字串作為索引鍵,可以增加對 json 資料庫的存取速度。
|
||||
///
|
||||
/// 如果傳入的字串當中包含 ASCII 下畫線符號的話,則表明該字串並非注音讀音字串,會被忽略處理。
|
||||
/// - parameters:
|
||||
/// - incoming: 傳入的未加密注音讀音字串。
|
||||
public static func cnvPhonabetToASCII(_ incoming: String) -> String {
|
||||
var strOutput = incoming
|
||||
if !strOutput.contains("_") {
|
||||
for entry in Self.dicPhonabet2ASCII {
|
||||
strOutput = strOutput.replacingOccurrences(of: entry.key, with: entry.value)
|
||||
}
|
||||
}
|
||||
return strOutput
|
||||
}
|
||||
|
||||
/// 內部函式,用以將被加密的注音讀音索引鍵進行解密。
|
||||
///
|
||||
/// 如果傳入的字串當中包含 ASCII 下畫線符號的話,則表明該字串並非注音讀音字串,會被忽略處理。
|
||||
/// - parameters:
|
||||
/// - incoming: 傳入的已加密注音讀音字串。
|
||||
public static 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 dicPhonabet2ASCII: [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",
|
||||
]
|
||||
|
||||
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": "˙",
|
||||
]
|
||||
}
|
||||
}
|
|
@ -7,36 +7,67 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import Foundation
|
||||
import Shared
|
||||
|
||||
extension LMAssembly {
|
||||
struct LMPlainBopomofo {
|
||||
@usableFromInline typealias DataMap = [String: [String: String]]
|
||||
let dataMap: DataMap
|
||||
public extension vChewingLM {
|
||||
@frozen struct LMPlainBopomofo {
|
||||
public private(set) var filePath: String?
|
||||
var dataMap: [String: String] = [:]
|
||||
|
||||
public var count: Int { dataMap.count }
|
||||
|
||||
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 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] = []
|
||||
let subKey = isCHS ? "S" : "T"
|
||||
if let arrRangeRecords: String = dataMap[key]?[subKey] {
|
||||
if let arrRangeRecords: String = dataMap[key]?.trimmingCharacters(in: .newlines) {
|
||||
pairs.append(contentsOf: arrRangeRecords.map(\.description))
|
||||
}
|
||||
// 這裡不做去重複處理,因為倚天中文系統注音排序適應者們已經形成了肌肉記憶。
|
||||
return pairs
|
||||
return pairs.deduplicated
|
||||
}
|
||||
|
||||
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
|
||||
// requirements defined in MIT License.
|
||||
|
||||
extension LMAssembly {
|
||||
struct LMReplacements {
|
||||
import Shared
|
||||
|
||||
public extension vChewingLM {
|
||||
@frozen struct LMReplacements {
|
||||
public private(set) var filePath: String?
|
||||
var rangeMap: [String: Range<String.Index>] = [:]
|
||||
var strData: String = ""
|
||||
|
@ -33,8 +35,8 @@ extension LMAssembly {
|
|||
replaceData(textData: rawStrData)
|
||||
} catch {
|
||||
filePath = oldPath
|
||||
vCLMLog("\(error)")
|
||||
vCLMLog("↑ Exception happened when reading data at: \(path).")
|
||||
vCLog("\(error)")
|
||||
vCLog("↑ Exception happened when reading data at: \(path).")
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -70,7 +72,7 @@ extension LMAssembly {
|
|||
do {
|
||||
try strData.write(toFile: filePath, atomically: true, encoding: .utf8)
|
||||
} 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 {
|
||||
strDump += strData[entry.value] + "\n"
|
||||
}
|
||||
vCLMLog(strDump)
|
||||
vCLog(strDump)
|
||||
}
|
||||
|
||||
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 Megrez
|
||||
import Shared
|
||||
|
||||
// MARK: - Public Types.
|
||||
|
||||
public extension LMAssembly {
|
||||
struct OverrideSuggestion {
|
||||
public var candidates = [(String, Megrez.Unigram)]()
|
||||
public var forceHighScoreOverride = false
|
||||
public var isEmpty: Bool { candidates.isEmpty }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - LMUserOverride Class Definition.
|
||||
|
||||
extension LMAssembly {
|
||||
public extension vChewingLM {
|
||||
class LMUserOverride {
|
||||
// MARK: - Main
|
||||
|
||||
var mutCapacity: Int
|
||||
var mutDecayExponent: Double
|
||||
var mutLRUList: [KeyObservationPair] = []
|
||||
var mutLRUMap: [String: KeyObservationPair] = [:]
|
||||
let kDecayThreshold: Double = 1.0 / 1_048_576.0 // 衰減二十次之後差不多就失效了。
|
||||
var fileSaveLocationURL: URL?
|
||||
var fileSaveLocationURL: URL
|
||||
|
||||
public static let kObservedOverrideHalfLife: Double = 3600.0 * 6 // 6 小時半衰一次,能持續不到六天的記憶。
|
||||
|
||||
public init(capacity: Int = 500, decayConstant: Double = LMUserOverride.kObservedOverrideHalfLife, dataURL: URL? = nil) {
|
||||
public init(capacity: Int = 500, decayConstant: Double = LMUserOverride.kObservedOverrideHalfLife, dataURL: URL) {
|
||||
mutCapacity = max(capacity, 1) // Ensures that this integer value is always > 0.
|
||||
mutDecayExponent = log(0.5) / decayConstant
|
||||
fileSaveLocationURL = dataURL
|
||||
}
|
||||
|
||||
public func performObservation(
|
||||
walkedBefore: [Megrez.Node], walkedAfter: [Megrez.Node],
|
||||
cursor: Int, timestamp: Double, saveCallback: @escaping () -> Void
|
||||
) {
|
||||
// 參數合規性檢查。
|
||||
guard !walkedAfter.isEmpty, !walkedBefore.isEmpty else { return }
|
||||
guard walkedBefore.totalKeyCount == walkedAfter.totalKeyCount else { return }
|
||||
// 先判斷用哪種覆寫方法。
|
||||
var actualCursor = 0
|
||||
guard let currentNode = walkedAfter.findNode(at: cursor, target: &actualCursor) else { return }
|
||||
// 當前節點超過三個字的話,就不記憶了。在這種情形下,使用者可以考慮新增自訂語彙。
|
||||
guard currentNode.spanLength <= 3 else { return }
|
||||
// 前一個節點得從前一次爬軌結果當中來找。
|
||||
guard actualCursor > 0 else { return } // 該情況應該不會出現。
|
||||
let currentNodeIndex = actualCursor
|
||||
actualCursor -= 1
|
||||
var prevNodeIndex = 0
|
||||
guard let prevNode = walkedBefore.findNode(at: actualCursor, target: &prevNodeIndex) else { return }
|
||||
|
||||
let forceHighScoreOverride: Bool = currentNode.spanLength > prevNode.spanLength
|
||||
let breakingUp = currentNode.spanLength == 1 && prevNode.spanLength > 1
|
||||
|
||||
let targetNodeIndex = breakingUp ? currentNodeIndex : prevNodeIndex
|
||||
let key: String = vChewingLM.LMUserOverride.formObservationKey(
|
||||
walkedNodes: walkedAfter, headIndex: targetNodeIndex
|
||||
)
|
||||
guard !key.isEmpty else { return }
|
||||
doObservation(
|
||||
key: key, candidate: currentNode.currentUnigram.value, timestamp: timestamp,
|
||||
forceHighScoreOverride: forceHighScoreOverride, saveCallback: { saveCallback() }
|
||||
)
|
||||
}
|
||||
|
||||
public func fetchSuggestion(
|
||||
currentWalk: [Megrez.Node], cursor: Int, timestamp: Double
|
||||
) -> Suggestion {
|
||||
var headIndex = 0
|
||||
guard let nodeIter = currentWalk.findNode(at: cursor, target: &headIndex) else { return .init() }
|
||||
let key = vChewingLM.LMUserOverride.formObservationKey(walkedNodes: currentWalk, headIndex: headIndex)
|
||||
return getSuggestion(key: key, timestamp: timestamp, headReading: nodeIter.joinedKey())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private Structures
|
||||
|
||||
extension LMAssembly.LMUserOverride {
|
||||
extension vChewingLM.LMUserOverride {
|
||||
enum OverrideUnit: CodingKey { case count, timestamp, forceHighScoreOverride }
|
||||
enum ObservationUnit: CodingKey { case count, overrides }
|
||||
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 {
|
||||
func performObservation(
|
||||
walkedBefore: [Megrez.Node], walkedAfter: [Megrez.Node],
|
||||
cursor: Int, timestamp: Double, saveCallback: (() -> Void)? = nil
|
||||
) {
|
||||
// 參數合規性檢查。
|
||||
guard !walkedAfter.isEmpty, !walkedBefore.isEmpty else { return }
|
||||
guard walkedBefore.totalKeyCount == walkedAfter.totalKeyCount else { return }
|
||||
// 先判斷用哪種覆寫方法。
|
||||
var actualCursor = 0
|
||||
guard let currentNode = walkedAfter.findNode(at: cursor, target: &actualCursor) else { return }
|
||||
// 當前節點超過三個字的話,就不記憶了。在這種情形下,使用者可以考慮新增自訂語彙。
|
||||
guard currentNode.spanLength <= 3 else { return }
|
||||
// 前一個節點得從前一次爬軌結果當中來找。
|
||||
guard actualCursor > 0 else { return } // 該情況應該不會出現。
|
||||
let currentNodeIndex = actualCursor
|
||||
actualCursor -= 1
|
||||
var prevNodeIndex = 0
|
||||
guard let prevNode = walkedBefore.findNode(at: actualCursor, target: &prevNodeIndex) else { return }
|
||||
|
||||
let forceHighScoreOverride: Bool = currentNode.spanLength > prevNode.spanLength
|
||||
let breakingUp = currentNode.spanLength == 1 && prevNode.spanLength > 1
|
||||
|
||||
let targetNodeIndex = breakingUp ? currentNodeIndex : prevNodeIndex
|
||||
let key: String = LMAssembly.LMUserOverride.formObservationKey(
|
||||
walkedNodes: walkedAfter, headIndex: targetNodeIndex
|
||||
)
|
||||
guard !key.isEmpty else { return }
|
||||
doObservation(
|
||||
key: key, candidate: currentNode.currentUnigram.value, timestamp: timestamp,
|
||||
forceHighScoreOverride: forceHighScoreOverride, saveCallback: saveCallback
|
||||
)
|
||||
}
|
||||
|
||||
func fetchSuggestion(
|
||||
currentWalk: [Megrez.Node], cursor: Int, timestamp: Double
|
||||
) -> LMAssembly.OverrideSuggestion {
|
||||
var headIndex = 0
|
||||
guard let nodeIter = currentWalk.findNode(at: cursor, target: &headIndex) else { return .init() }
|
||||
let key = LMAssembly.LMUserOverride.formObservationKey(walkedNodes: currentWalk, headIndex: headIndex)
|
||||
return getSuggestion(key: key, timestamp: timestamp, headReading: nodeIter.joinedKey())
|
||||
}
|
||||
|
||||
func bleachSpecifiedSuggestions(targets: [String], saveCallback: (() -> Void)? = nil) {
|
||||
public extension vChewingLM.LMUserOverride {
|
||||
func bleachSpecifiedSuggestions(targets: [String], saveCallback: @escaping () -> Void) {
|
||||
if targets.isEmpty { return }
|
||||
for neta in mutLRUMap {
|
||||
for target in targets {
|
||||
|
@ -175,86 +166,82 @@ extension LMAssembly.LMUserOverride {
|
|||
}
|
||||
}
|
||||
resetMRUList()
|
||||
saveCallback?() ?? saveData()
|
||||
saveCallback()
|
||||
}
|
||||
|
||||
/// 自 LRU 辭典內移除所有的單元圖。
|
||||
func bleachUnigrams(saveCallback: (() -> Void)? = nil) {
|
||||
func bleachUnigrams(saveCallback: @escaping () -> Void) {
|
||||
for key in mutLRUMap.keys {
|
||||
if !key.contains("(),()") { continue }
|
||||
mutLRUMap.removeValue(forKey: key)
|
||||
}
|
||||
resetMRUList()
|
||||
saveCallback?() ?? saveData()
|
||||
saveCallback()
|
||||
}
|
||||
|
||||
func resetMRUList() {
|
||||
internal func resetMRUList() {
|
||||
mutLRUList.removeAll()
|
||||
for neta in mutLRUMap.reversed() {
|
||||
mutLRUList.append(neta.value)
|
||||
}
|
||||
}
|
||||
|
||||
func clearData(withURL fileURL: URL? = nil) {
|
||||
func clearData(withURL fileURL: URL) {
|
||||
mutLRUMap = .init()
|
||||
mutLRUList = .init()
|
||||
do {
|
||||
let nullData = "{}"
|
||||
guard let fileURL = fileURL ?? fileSaveLocationURL else {
|
||||
throw UOMError(rawValue: "given fileURL is invalid or nil.")
|
||||
}
|
||||
try nullData.write(to: fileURL, atomically: false, encoding: .utf8)
|
||||
} catch {
|
||||
vCLMLog("UOM Error: Unable to clear the data in the UOM file. Details: \(error)")
|
||||
vCLog("UOM Error: Unable to clear data. Details: \(error)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func saveData(toURL fileURL: URL? = nil) {
|
||||
guard let fileURL: URL = fileURL ?? fileSaveLocationURL else {
|
||||
vCLMLog("UOM saveData() failed. At least the file Save URL is not set for the current UOM.")
|
||||
return
|
||||
}
|
||||
// 此處不要使用 JSONSerialization,不然執行緒會炸掉。
|
||||
let encoder = JSONEncoder()
|
||||
do {
|
||||
guard let jsonData = try? encoder.encode(mutLRUMap) else { return }
|
||||
let fileURL: URL = fileURL ?? fileSaveLocationURL
|
||||
try jsonData.write(to: fileURL, options: .atomic)
|
||||
} catch {
|
||||
vCLMLog("UOM Error: Unable to save data, abort saving. Details: \(error)")
|
||||
vCLog("UOM Error: Unable to save data, abort saving. Details: \(error)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func loadData(fromURL fileURL: URL? = nil) {
|
||||
guard let fileURL: URL = fileURL ?? fileSaveLocationURL else {
|
||||
vCLMLog("UOM loadData() failed. At least the file Load URL is not set for the current UOM.")
|
||||
return
|
||||
}
|
||||
func loadData(fromURL fileURL: URL) {
|
||||
// 此處不要使用 JSONSerialization,不然執行緒會炸掉。
|
||||
let decoder = JSONDecoder()
|
||||
do {
|
||||
let data = try Data(contentsOf: fileURL, options: .mappedIfSafe)
|
||||
if ["", "{}"].contains(String(data: data, encoding: .utf8)) { return }
|
||||
guard let jsonResult = try? decoder.decode([String: KeyObservationPair].self, from: data) else {
|
||||
vCLMLog("UOM Error: Read file content type invalid, abort loading.")
|
||||
vCLog("UOM Error: Read file content type invalid, abort loading.")
|
||||
return
|
||||
}
|
||||
mutLRUMap = jsonResult
|
||||
resetMRUList()
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
key: String, candidate: String, timestamp: Double, forceHighScoreOverride: Bool,
|
||||
saveCallback: (() -> Void)?
|
||||
saveCallback: @escaping () -> Void
|
||||
) {
|
||||
guard mutLRUMap[key] != nil else {
|
||||
var observation: Observation = .init()
|
||||
|
@ -270,8 +257,8 @@ extension LMAssembly.LMUserOverride {
|
|||
mutLRUMap.removeValue(forKey: mutLRUList[mutLRUList.endIndex - 1].key)
|
||||
mutLRUList.removeLast()
|
||||
}
|
||||
vCLMLog("UOM: Observation finished with new observation: \(key)")
|
||||
saveCallback?() ?? saveData()
|
||||
vCLog("UOM: Observation finished with new observation: \(key)")
|
||||
saveCallback()
|
||||
return
|
||||
}
|
||||
// 這裡還是不要做 decayCallback 判定「是否不急著更新觀察」了,不然會在嘗試覆寫掉錯誤的記憶時失敗。
|
||||
|
@ -281,12 +268,12 @@ extension LMAssembly.LMUserOverride {
|
|||
)
|
||||
mutLRUList.insert(theNeta, at: 0)
|
||||
mutLRUMap[key] = theNeta
|
||||
vCLMLog("UOM: Observation finished with existing observation: \(key)")
|
||||
saveCallback?() ?? saveData()
|
||||
vCLog("UOM: Observation finished with existing observation: \(key)")
|
||||
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() }
|
||||
let observation: Observation = kvPair.observation
|
||||
var candidates: [(String, Megrez.Unigram)] = .init()
|
||||
|
@ -399,10 +386,3 @@ extension LMAssembly.LMUserOverride {
|
|||
return result
|
||||
}
|
||||
}
|
||||
|
||||
struct UOMError: LocalizedError {
|
||||
var rawValue: String
|
||||
var errorDescription: String? {
|
||||
NSLocalizedString("rawValue", comment: "")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
// 下述詞頻資料取自 libTaBE 資料庫 (http://sourceforge.net/projects/libtabe/)
|
||||
// (2002 最終版). 該專案於 1999 年由 Pai-Hsiang Hsiao 發起、以 BSD 授權發行。
|
||||
|
||||
import Foundation
|
||||
|
||||
let sqlTestCoreLMData: String = """
|
||||
PRAGMA foreign_keys=OFF;
|
||||
BEGIN TRANSACTION;
|
||||
CREATE TABLE DATA_MAIN (
|
||||
theKey TEXT NOT NULL,
|
||||
theDataCHS TEXT,
|
||||
theDataCHT TEXT,
|
||||
theDataCNS TEXT,
|
||||
theDataMISC TEXT,
|
||||
theDataSYMB TEXT,
|
||||
theDataCHEW TEXT,
|
||||
PRIMARY KEY (theKey)
|
||||
) WITHOUT ROWID;
|
||||
INSERT INTO DATA_MAIN VALUES('CuP-niF2-bi','-7.375 吹牛逼\t-7.399 吹牛屄','-7.375 吹牛逼\t-7.399 吹牛屄','','','🌳🆕🐝','');
|
||||
INSERT INTO DATA_MAIN VALUES('Ze4','-6.0 這','-6.0 這',NULL,NULL,NULL,NULL);
|
||||
INSERT INTO DATA_MAIN VALUES('Ze4-iN4','-6.0 這樣','-6.0 這樣',NULL,NULL,NULL,NULL);
|
||||
INSERT INTO DATA_MAIN VALUES('ZuL','-5.809297 中\t-99.0 終\t-9.87758 鐘\t-9.685671 鍾\t-99.0 盅\t-99.0 忠','-5.809297 中\t-99.0 終\t-9.87758 鐘\t-9.685671 鍾\t-99.0 盅\t-99.0 忠',NULL,NULL,NULL,NULL);
|
||||
INSERT INTO DATA_MAIN VALUES('_punctuation_list',' \t,\t、\t。',' \t,\t、\t。','','','','');
|
||||
INSERT INTO DATA_MAIN VALUES('de5','-3.516024 的\t-7.427179 得','-3.516024 的\t-7.427179 得',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('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('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('guL','-8.381971 共\t-8.501463 供\t-8.858181 紅\t-7.877973 公\t-7.822167 工\t-99.0 攻\t-99.0 功\t-99.0 宮\t-99.0 弓\t-99.0 恭\t-99.0 躬','-8.381971 共\t-8.501463 供\t-8.858181 紅\t-7.877973 公\t-7.822167 工\t-99.0 攻\t-99.0 功\t-99.0 宮\t-99.0 弓\t-99.0 恭\t-99.0 躬',NULL,NULL,NULL,NULL);
|
||||
INSERT INTO DATA_MAIN VALUES('guL-s','-6.299461 公司','-6.299461 公司',NULL,NULL,NULL,NULL);
|
||||
INSERT INTO DATA_MAIN VALUES('iN4','-6.0 樣','-6.0 樣',NULL,NULL,NULL,NULL);
|
||||
INSERT INTO DATA_MAIN VALUES('ji4','-99.0 既\t-7.608341 際\t-99.0 季\t-10.939895 騎\t-99.0 記\t-99.0 寄\t-9.715317 繼\t-7.926683 計\t-8.373022 暨\t-10.425662 繫\t-8.888722 劑\t-10.204425 祭\t-99.0 忌\t-8.450826 技\t-12.045357 冀\t-99.0 妓\t-9.517568 濟\t-12.021587 薊','-99.0 既\t-7.608341 際\t-99.0 季\t-10.939895 騎\t-99.0 記\t-99.0 寄\t-9.715317 繼\t-7.926683 計\t-8.373022 暨\t-10.425662 繫\t-8.888722 劑\t-10.204425 祭\t-99.0 忌\t-8.450826 技\t-12.045357 冀\t-99.0 妓\t-9.517568 濟\t-12.021587 薊',NULL,NULL,NULL,NULL);
|
||||
INSERT INTO DATA_MAIN VALUES('ji4-guL','-13.336653 濟公','-13.336653 濟公',NULL,NULL,NULL,NULL);
|
||||
INSERT INTO DATA_MAIN VALUES('jiM4','-3.676169 教\t-3.24869962 較','-3.676169 教\t-3.24869962 較',NULL,NULL,NULL,NULL);
|
||||
INSERT INTO DATA_MAIN VALUES('jiM4-v4','-3.32220565 教育','-3.32220565 教育',NULL,NULL,NULL,NULL);
|
||||
INSERT INTO DATA_MAIN VALUES('jiN3','-9.164384 講\t-8.690941 獎\t-10.127828 蔣\t-12.492933 槳','-9.164384 講\t-8.690941 獎\t-10.127828 蔣\t-12.492933 槳',NULL,NULL,NULL,NULL);
|
||||
INSERT INTO DATA_MAIN VALUES('jiN3-jiT','-10.344678 獎金','-10.344678 獎金',NULL,NULL,NULL,NULL);
|
||||
INSERT INTO DATA_MAIN VALUES('jiT','-8.034095 今\t-7.290109 金\t-99.0 斤\t-10.711079 禁\t-11.378321 浸\t-11.07489 筋\t-99.0 巾\t-12.784206 襟','-8.034095 今\t-7.290109 金\t-99.0 斤\t-10.711079 禁\t-11.378321 浸\t-11.07489 筋\t-99.0 巾\t-12.784206 襟',NULL,NULL,NULL,NULL);
|
||||
INSERT INTO DATA_MAIN VALUES('ke','-10.574273 顆\t-11.504072 棵\t-10.450457 刻\t-7.171052 科\t-99.0 柯','-10.574273 顆\t-11.504072 棵\t-10.450457 刻\t-7.171052 科\t-99.0 柯',NULL,NULL,NULL,NULL);
|
||||
INSERT INTO DATA_MAIN VALUES('ke-ji4','-6.736613 科技','-6.736613 科技',NULL,NULL,NULL,NULL);
|
||||
INSERT INTO DATA_MAIN VALUES('ke-ke','-8.0 顆顆','-8.0 顆顆',NULL,NULL,NULL,NULL);
|
||||
INSERT INTO DATA_MAIN VALUES('mi4','-4.6231 蜜','-4.6231 蜜',NULL,NULL,NULL,NULL);
|
||||
INSERT INTO DATA_MAIN VALUES('mi4-fL','-11.0 🐝','-11.0 🐝',NULL,NULL,NULL,NULL);
|
||||
INSERT INTO DATA_MAIN VALUES('nP-nP','','','','','☉☉','ㄋㄟㄋㄟ');
|
||||
INSERT INTO DATA_MAIN VALUES('ni3','-6.0 你','-6.0 你',NULL,NULL,NULL,NULL);
|
||||
INSERT INTO DATA_MAIN VALUES('ni3-Ze4','-9.0 你這','-9.0 你這',NULL,NULL,NULL,NULL);
|
||||
INSERT INTO DATA_MAIN VALUES('niD2','-6.086515 年\t-11.336864 黏\t-11.28574 粘','-6.086515 年\t-11.336864 黏\t-11.28574 粘',NULL,NULL,NULL,NULL);
|
||||
INSERT INTO DATA_MAIN VALUES('niD2-ZuL','-11.668947 年終\t-11.373044 年中','-11.668947 年終\t-11.373044 年中',NULL,NULL,NULL,NULL);
|
||||
INSERT INTO DATA_MAIN VALUES('s','-9.495858 絲\t-9.006414 思\t-99.0 私\t-8.091803 斯\t-99.0 司\t-13.513987 嘶\t-12.259095 撕','-9.495858 絲\t-9.006414 思\t-99.0 私\t-8.091803 斯\t-99.0 司\t-13.513987 嘶\t-12.259095 撕',NULL,NULL,NULL,NULL);
|
||||
INSERT INTO DATA_MAIN VALUES('v4','-3.30192952 育','-3.30192952 育',NULL,NULL,NULL,NULL);
|
||||
CREATE TABLE DATA_REV (
|
||||
theChar TEXT NOT NULL,
|
||||
theReadings TEXT NOT NULL,
|
||||
PRIMARY KEY (theChar)
|
||||
) WITHOUT ROWID;
|
||||
INSERT INTO DATA_REV VALUES('和','huo2\the5\thuo\tduL\the2\the4\thD4\thu2\thuo5\thuo4');
|
||||
COMMIT;
|
||||
"""
|
|
@ -0,0 +1,25 @@
|
|||
// (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 enum vChewingLM {
|
||||
enum FileErrors: Error {
|
||||
case fileHandleError(String)
|
||||
}
|
||||
|
||||
public enum ReplacableUserDataType: String, CaseIterable, Identifiable {
|
||||
public var id: ObjectIdentifier { .init(rawValue as AnyObject) }
|
||||
|
||||
case thePhrases
|
||||
case theFilter
|
||||
case theReplacements
|
||||
case theAssociates
|
||||
case theSymbols
|
||||
}
|
||||
}
|
|
@ -1,94 +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 SQLite3
|
||||
|
||||
public enum LMAssembly {
|
||||
enum FileErrors: Error {
|
||||
case fileHandleError(String)
|
||||
}
|
||||
|
||||
public enum ReplacableUserDataType: String, CaseIterable, Identifiable {
|
||||
public var id: ObjectIdentifier { .init(rawValue as AnyObject) }
|
||||
public var localizedDescription: String { NSLocalizedString(rawValue, comment: "") }
|
||||
|
||||
case thePhrases
|
||||
case theFilter
|
||||
case theReplacements
|
||||
case theAssociates
|
||||
case theSymbols
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - String as SQL Command
|
||||
|
||||
extension String {
|
||||
@discardableResult func runAsSQLExec(dbPointer ptrDB: inout OpaquePointer?) -> Bool {
|
||||
ptrDB != nil && sqlite3_exec(ptrDB, self, nil, nil, nil) == SQLITE_OK
|
||||
}
|
||||
|
||||
@discardableResult func runAsSQLPreparedStep(dbPointer ptrDB: inout OpaquePointer?) -> Bool {
|
||||
guard ptrDB != nil else { return false }
|
||||
return performStatement { ptrStmt in
|
||||
sqlite3_prepare_v2(ptrDB, self, -1, &ptrStmt, nil) == SQLITE_OK && sqlite3_step(ptrStmt) == SQLITE_DONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == String {
|
||||
@discardableResult func runAsSQLPreparedSteps(dbPointer ptrDB: inout OpaquePointer?) -> Bool {
|
||||
guard ptrDB != nil else { return false }
|
||||
guard "begin;".runAsSQLExec(dbPointer: &ptrDB) else { return false }
|
||||
defer {
|
||||
let looseEnds = sqlite3_exec(ptrDB, "commit;", nil, nil, nil) == SQLITE_OK
|
||||
assert(looseEnds)
|
||||
}
|
||||
|
||||
for strStmt in self {
|
||||
let thisResult = performStatement { ptrStmt in
|
||||
sqlite3_prepare_v2(ptrDB, strStmt, -1, &ptrStmt, nil) == SQLITE_OK && sqlite3_step(ptrStmt) == SQLITE_DONE
|
||||
}
|
||||
guard thisResult else {
|
||||
vCLMLog("SQL Query Error. Statement: \(strStmt)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Safe APIs for using SQLite Statements.
|
||||
|
||||
func performStatement(_ handler: (inout OpaquePointer?) -> Bool) -> Bool {
|
||||
var ptrStmt: OpaquePointer?
|
||||
defer {
|
||||
sqlite3_finalize(ptrStmt)
|
||||
ptrStmt = nil
|
||||
}
|
||||
return handler(&ptrStmt)
|
||||
}
|
||||
|
||||
func performStatementSansResult(_ handler: (inout OpaquePointer?) -> Void) {
|
||||
var ptrStmt: OpaquePointer?
|
||||
defer {
|
||||
sqlite3_finalize(ptrStmt)
|
||||
ptrStmt = nil
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -1,75 +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 XCTest
|
||||
|
||||
@testable import LangModelAssembly
|
||||
|
||||
final class InputTokenTests: XCTestCase {
|
||||
func testTranslatingTokens_1_TimeZone() throws {
|
||||
print("測試時區俗稱:" + "MACRO@TIMEZONE_SHORTENED".parseAsInputToken(isCHS: false).description)
|
||||
print("測試時區全稱:" + "MACRO@TIMEZONE".parseAsInputToken(isCHS: false).description)
|
||||
}
|
||||
|
||||
func testTranslatingTokens_2_TimeNow() throws {
|
||||
print("測試時間時分:" + "MACRO@TIME_SHORTENED".parseAsInputToken(isCHS: false).description)
|
||||
print("測試帶秒時間:" + "MACRO@TIME".parseAsInputToken(isCHS: true).description)
|
||||
}
|
||||
|
||||
func testTranslatingTokens_3_Date() throws {
|
||||
print("測試農曆:" + "MACRO@DATE_LUNA".parseAsInputToken(isCHS: true).description)
|
||||
print("測試二戰勝利紀年:" + "MACRO@DATE_YEARDELTA:-1945".parseAsInputToken(isCHS: true).description)
|
||||
print("測試短日期之135天前:" + "MACRO@DATE_DAYDELTA:-135_SHORTENED".parseAsInputToken(isCHS: true).description)
|
||||
print("測試長日期之135天前:" + "MACRO@DATE_DAYDELTA:-135".parseAsInputToken(isCHS: true).description)
|
||||
print("測試短日期之今天:" + "MACRO@DATE_SHORTENED".parseAsInputToken(isCHS: true).description)
|
||||
print("測試長日期之今天:" + "MACRO@DATE".parseAsInputToken(isCHS: true).description)
|
||||
print("測試短日期之明天:" + "MACRO@DATE_SHORTENED_DAYDELTA:1".parseAsInputToken(isCHS: true).description)
|
||||
print("測試長日期之明天:" + "MACRO@DATE_DAYDELTA:1".parseAsInputToken(isCHS: true).description)
|
||||
print("測試短日期之明年:" + "MACRO@DATE_SHORTENED_YEARDELTA:1".parseAsInputToken(isCHS: true).description)
|
||||
print("測試長日期之明年:" + "MACRO@DATE_YEARDELTA:1".parseAsInputToken(isCHS: true).description)
|
||||
}
|
||||
|
||||
func testTranslatingTokens_4_Week() throws {
|
||||
print("測試今天星期幾:" + "MACRO@WEEK".parseAsInputToken(isCHS: false).description)
|
||||
print("測試今天週幾:" + "MACRO@WEEK_SHORTENED".parseAsInputToken(isCHS: false).description)
|
||||
print("測試明天星期幾:" + "MACRO@WEEK_DAYDELTA:1".parseAsInputToken(isCHS: false).description)
|
||||
print("測試明天週幾:" + "MACRO@WEEK_SHORTENED_DAYDELTA:1".parseAsInputToken(isCHS: false).description)
|
||||
print("測試後天星期幾:" + "MACRO@WEEK_DAYDELTA:+2".parseAsInputToken(isCHS: false).description)
|
||||
print("測試後天週幾:" + "MACRO@WEEK_SHORTENED_DAYDELTA:+2".parseAsInputToken(isCHS: false).description)
|
||||
}
|
||||
|
||||
func testTranslatingTokens_5_Year() throws {
|
||||
print("測試今年:" + "MACRO@YEAR".parseAsInputToken(isCHS: false).description)
|
||||
print("測試今年干支:" + "MACRO@YEAR_GANZHI".parseAsInputToken(isCHS: false).description)
|
||||
print("測試今年生肖:" + "MACRO@YEAR_ZODIAC".parseAsInputToken(isCHS: false).description)
|
||||
print("測試一千年以前:" + "MACRO@YEAR_YEARDELTA:-1000".parseAsInputToken(isCHS: false).description)
|
||||
print("測試一千年以前干支:" + "MACRO@YEAR_GANZHI_YEARDELTA:-1000".parseAsInputToken(isCHS: false).description)
|
||||
print("測試一千年以前生肖:" + "MACRO@YEAR_ZODIAC_YEARDELTA:-1000".parseAsInputToken(isCHS: false).description)
|
||||
print("測試一千年以後:" + "MACRO@YEAR_YEARDELTA:1000".parseAsInputToken(isCHS: false).description)
|
||||
print("測試一千年以後干支:" + "MACRO@YEAR_GANZHI_YEARDELTA:1000".parseAsInputToken(isCHS: false).description)
|
||||
print("測試一千年以後生肖:" + "MACRO@YEAR_ZODIAC_YEARDELTA:1000".parseAsInputToken(isCHS: false).description)
|
||||
}
|
||||
|
||||
func testGeneratedResultsFromLMInstantiator() throws {
|
||||
let instance = LMAssembly.LMInstantiator(isCHS: true)
|
||||
XCTAssertTrue(LMAssembly.LMInstantiator.connectToTestSQLDB())
|
||||
instance.setOptions { config in
|
||||
config.isCNSEnabled = false
|
||||
config.isSymbolEnabled = false
|
||||
}
|
||||
instance.insertTemporaryData(
|
||||
keyArray: ["ㄐㄧㄣ", "ㄊㄧㄢ", "ㄖˋ", "ㄑㄧˊ"],
|
||||
unigram: .init(value: "MACRO@DATE_YEARDELTA:-1945", score: -97.5),
|
||||
isFiltering: false
|
||||
)
|
||||
let x = instance.unigramsFor(keyArray: ["ㄐㄧㄣ", "ㄊㄧㄢ", "ㄖˋ", "ㄑㄧˊ"]).description
|
||||
print(x)
|
||||
LMAssembly.LMInstantiator.disconnectSQLDB()
|
||||
}
|
||||
}
|
|
@ -20,7 +20,7 @@ private let testDataPath: String = packageRootPath + "/Tests/TestCINData/"
|
|||
final class LMCassetteTests: XCTestCase {
|
||||
func testCassetteLoadWubi86() throws {
|
||||
let pathCINFile = testDataPath + "wubi.cin"
|
||||
var lmCassette = LMAssembly.LMCassette()
|
||||
var lmCassette = vChewingLM.LMCassette()
|
||||
NSLog("LMCassette: Start loading CIN.")
|
||||
lmCassette.open(pathCINFile)
|
||||
NSLog("LMCassette: Finished loading CIN. Entries: \(lmCassette.count)")
|
||||
|
@ -41,13 +41,13 @@ final class LMCassetteTests: XCTestCase {
|
|||
|
||||
func testCassetteLoadArray30() throws {
|
||||
let pathCINFile = testDataPath + "array30.cin2"
|
||||
var lmCassette = LMAssembly.LMCassette()
|
||||
var lmCassette = vChewingLM.LMCassette()
|
||||
NSLog("LMCassette: Start loading CIN.")
|
||||
lmCassette.open(pathCINFile)
|
||||
NSLog("LMCassette: Finished loading CIN. Entries: \(lmCassette.count)")
|
||||
XCTAssertFalse(lmCassette.quickDefMap.isEmpty)
|
||||
print(lmCassette.quickSetsFor(key: ",.") ?? "")
|
||||
XCTAssertEqual(lmCassette.keyNameMap.count, 31)
|
||||
XCTAssertEqual(lmCassette.keyNameMap.count, 41)
|
||||
XCTAssertEqual(lmCassette.charDefMap.count, 29491)
|
||||
XCTAssertEqual(lmCassette.charDefWildcardMap.count, 11946)
|
||||
XCTAssertEqual(lmCassette.octagramMap.count, 0)
|
||||
|
|
|
@ -38,7 +38,7 @@ private let sampleData: String = #"""
|
|||
|
||||
final class LMCoreEXTests: XCTestCase {
|
||||
func testLMCoreEXAsFactoryCoreDict() throws {
|
||||
var lmTest = LMAssembly.LMCoreEX(
|
||||
var lmTest = vChewingLM.LMCoreEX(
|
||||
reverse: false, consolidate: false, defaultScore: 0, forceDefaultScore: false
|
||||
)
|
||||
lmTest.replaceData(textData: sampleData)
|
||||
|
|
|
@ -1,64 +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 XCTest
|
||||
|
||||
@testable import LangModelAssembly
|
||||
|
||||
private let strBloatingKey: [String] = ["ㄔㄨㄟ", "ㄋㄧㄡˊ", "ㄅㄧ"]
|
||||
private let strHaninSymbolMenuKey: [String] = ["_punctuation_list"]
|
||||
private let strRefutationKey: [String] = ["ㄉㄨㄟˇ"]
|
||||
private let strBoobsKey: [String] = ["ㄋㄟ", "ㄋㄟ"]
|
||||
private let expectedReverseLookupResults: [String] = [
|
||||
"ㄏㄨㄛˊ", "ㄏㄜ˙", "ㄏㄨㄛ", "ㄉㄨㄥ", "ㄏㄜˊ",
|
||||
"ㄏㄜˋ", "ㄏㄢˋ", "ㄏㄨˊ", "ㄏㄨㄛ˙", "ㄏㄨㄛˋ",
|
||||
]
|
||||
|
||||
final class LMInstantiatorSQLTests: XCTestCase {
|
||||
func testSQL() throws {
|
||||
let instance = LMAssembly.LMInstantiator(isCHS: true)
|
||||
XCTAssertTrue(LMAssembly.LMInstantiator.connectToTestSQLDB())
|
||||
instance.setOptions { config in
|
||||
config.isCNSEnabled = false
|
||||
config.isSymbolEnabled = false
|
||||
}
|
||||
XCTAssertEqual(instance.unigramsFor(keyArray: strBloatingKey).description, "[(吹牛逼,-7.375), (吹牛屄,-7.399)]")
|
||||
XCTAssertEqual(instance.unigramsFor(keyArray: strHaninSymbolMenuKey)[1].description, "(,,-9.9)")
|
||||
XCTAssertEqual(instance.unigramsFor(keyArray: strRefutationKey).description, "[(㨃,-9.544)]")
|
||||
XCTAssertEqual(instance.unigramsFor(keyArray: strBoobsKey).description, "[(ㄋㄟㄋㄟ,-1.0)]")
|
||||
instance.setOptions { config in
|
||||
config.isCNSEnabled = true
|
||||
config.isSymbolEnabled = true
|
||||
}
|
||||
XCTAssertEqual(instance.unigramsFor(keyArray: strBloatingKey).last?.description, "(🌳🆕🐝,-13.0)")
|
||||
XCTAssertEqual(instance.unigramsFor(keyArray: strHaninSymbolMenuKey)[1].description, "(,,-9.9)")
|
||||
XCTAssertEqual(instance.unigramsFor(keyArray: strRefutationKey).count, 10)
|
||||
XCTAssertEqual(instance.unigramsFor(keyArray: strBoobsKey).last?.description, "(☉☉,-13.0)")
|
||||
// 再測試反查。
|
||||
XCTAssertEqual(LMAssembly.LMInstantiator.getFactoryReverseLookupData(with: "和"), expectedReverseLookupResults)
|
||||
LMAssembly.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,15 +17,15 @@ private let halfLife: Double = 5400
|
|||
private let nullURL = URL(fileURLWithPath: "/dev/null")
|
||||
|
||||
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: {})
|
||||
}
|
||||
|
||||
func testUOM_1_BasicOps() throws {
|
||||
let uom = LMAssembly.LMUserOverride(capacity: capacity, decayConstant: Double(halfLife), dataURL: nullURL)
|
||||
let key = "((ㄕㄣˊ-ㄌㄧˇ-ㄌㄧㄥˊ-ㄏㄨㄚˊ,神里綾華),(ㄉㄜ˙,的),ㄍㄡˇ)"
|
||||
let headReading = "ㄍㄡˇ"
|
||||
let expectedSuggestion = "狗"
|
||||
let uom = vChewingLM.LMUserOverride(capacity: capacity, decayConstant: Double(halfLife), dataURL: nullURL)
|
||||
let key = "((ㄍㄨㄥ-ㄙ,公司),(ㄉㄜ˙,的),ㄋㄧㄢˊ-ㄓㄨㄥ)"
|
||||
let headReading = "ㄋㄧㄢˊ-ㄓㄨㄥ"
|
||||
let expectedSuggestion = "年終"
|
||||
observe(who: uom, key: key, candidate: expectedSuggestion, timestamp: nowTimeStamp)
|
||||
var suggested = uom.getSuggestion(key: key, timestamp: nowTimeStamp, headReading: headReading)
|
||||
XCTAssertEqual(Set(suggested.candidates.map(\.1.value)).first ?? "", expectedSuggestion)
|
||||
|
@ -45,11 +45,11 @@ final class LMUserOverrideTests: XCTestCase {
|
|||
}
|
||||
|
||||
func testUOM_2_NewestAgainstRepeatedlyUsed() throws {
|
||||
let uom = LMAssembly.LMUserOverride(capacity: capacity, decayConstant: Double(halfLife), dataURL: nullURL)
|
||||
let key = "((ㄕㄣˊ-ㄌㄧˇ-ㄌㄧㄥˊ-ㄏㄨㄚˊ,神里綾華),(ㄉㄜ˙,的),ㄍㄡˇ)"
|
||||
let headReading = "ㄍㄡˇ"
|
||||
let valRepeatedlyUsed = "狗" // 更常用
|
||||
let valNewest = "苟" // 最近偶爾用了一次
|
||||
let uom = vChewingLM.LMUserOverride(capacity: capacity, decayConstant: Double(halfLife), dataURL: nullURL)
|
||||
let key = "((ㄍㄨㄥ-ㄙ,公司),(ㄉㄜ˙,的),ㄋㄧㄢˊ-ㄓㄨㄥ)"
|
||||
let headReading = "ㄋㄧㄢˊ-ㄓㄨㄥ"
|
||||
let valRepeatedlyUsed = "年終" // 更常用
|
||||
let valNewest = "年中" // 最近偶爾用了一次
|
||||
let stamps: [Double] = [0, 0.5, 2, 2.5, 4, 4.5, 5.3].map { nowTimeStamp + halfLife * $0 }
|
||||
stamps.forEach { stamp in
|
||||
observe(who: uom, key: key, candidate: valRepeatedlyUsed, timestamp: stamp)
|
||||
|
@ -62,6 +62,8 @@ final class LMUserOverrideTests: XCTestCase {
|
|||
}
|
||||
// 試試看偶爾選了不常用的詞的話、是否會影響上文所生成的有一定強效的記憶。
|
||||
observe(who: uom, key: key, candidate: valNewest, timestamp: nowTimeStamp + halfLife * 23.4)
|
||||
suggested = uom.getSuggestion(key: key, timestamp: nowTimeStamp + halfLife * 23.6, headReading: headReading)
|
||||
XCTAssertEqual(Set(suggested.candidates.map(\.1.value)).first ?? "", valNewest)
|
||||
suggested = uom.getSuggestion(key: key, timestamp: nowTimeStamp + halfLife * 26, headReading: headReading)
|
||||
XCTAssertEqual(Set(suggested.candidates.map(\.1.value)).first ?? "", valNewest)
|
||||
suggested = uom.getSuggestion(key: key, timestamp: nowTimeStamp + halfLife * 50, headReading: headReading)
|
||||
|
@ -70,11 +72,11 @@ final class LMUserOverrideTests: XCTestCase {
|
|||
}
|
||||
|
||||
func testUOM_3_LRUTable() throws {
|
||||
let a = (key: "((ㄕㄣˊ-ㄌㄧˇ-ㄌㄧㄥˊ-ㄏㄨㄚˊ,神里綾華),(ㄉㄜ˙,的),ㄍㄡˇ)", value: "狗", head: "ㄍㄡˇ")
|
||||
let b = (key: "((ㄆㄞˋ-ㄇㄥˊ,派蒙),(ㄉㄜ˙,的),ㄐㄧㄤˇ-ㄐㄧㄣ)", value: "伙食費", head: "ㄏㄨㄛˇ-ㄕˊ-ㄈㄟˋ")
|
||||
let c = (key: "((ㄍㄨㄛˊ-ㄅㄥ,國崩),(ㄉㄜ˙,的),ㄇㄠˋ-ㄗ˙)", value: "帽子", head: "ㄇㄠˋ-ㄗ˙")
|
||||
let a = (key: "((ㄍㄨㄥ-ㄙ,公司),(ㄉㄜ˙,的),ㄋㄧㄢˊ-ㄓㄨㄥ)", value: "年終", head: "ㄋㄧㄢˊ-ㄓㄨㄥ")
|
||||
let b = (key: "((ㄑㄧˋ-ㄧㄝˋ,企業),(ㄉㄜ˙,的),ㄐㄧㄤˇ-ㄐㄧㄣ)", value: "獎金", head: "ㄐㄧㄤˇ-ㄐㄧㄣ")
|
||||
let c = (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: b.key, candidate: b.value, timestamp: nowTimeStamp + halfLife * 1)
|
||||
observe(who: uom, key: c.key, candidate: c.value, timestamp: nowTimeStamp + halfLife * 2)
|
||||
|
|
|
@ -1,30 +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 XCTest
|
||||
|
||||
@testable import LangModelAssembly
|
||||
|
||||
final class LMInstantiatorNumericPadTests: XCTestCase {
|
||||
func testSQL() throws {
|
||||
let instance = LMAssembly.LMInstantiator(isCHS: true)
|
||||
instance.setOptions { config in
|
||||
config.numPadFWHWStatus = nil
|
||||
}
|
||||
XCTAssertEqual(instance.unigramsFor(keyArray: ["_NumPad_0"]).description, "[]")
|
||||
instance.setOptions { config in
|
||||
config.numPadFWHWStatus = true
|
||||
}
|
||||
XCTAssertEqual(instance.unigramsFor(keyArray: ["_NumPad_0"]).description, "[(0,0.0), (0,-0.1)]")
|
||||
instance.setOptions { config in
|
||||
config.numPadFWHWStatus = false
|
||||
}
|
||||
XCTAssertEqual(instance.unigramsFor(keyArray: ["_NumPad_0"]).description, "[(0,0.0), (0,-0.1)]")
|
||||
}
|
||||
}
|
|
@ -16,38 +16,47 @@
|
|||
%phase_auto_skip_endkey
|
||||
%flag_disp_full_match
|
||||
%flag_disp_partial_match
|
||||
%keys_to_directly_commit !@#$%^&*()-_=+[{]}\|:'"<>?
|
||||
%keyname begin
|
||||
a 1-
|
||||
b 5v
|
||||
c 3v
|
||||
d 3-
|
||||
e 3^
|
||||
f 4-
|
||||
g 5-
|
||||
h 6-
|
||||
i 8^
|
||||
j 7-
|
||||
k 8-
|
||||
l 9-
|
||||
m 7v
|
||||
n 6v
|
||||
o 9^
|
||||
p 0^
|
||||
q 1^
|
||||
r 4^
|
||||
s 2-
|
||||
t 5^
|
||||
u 7^
|
||||
v 4v
|
||||
w 2^
|
||||
x 2v
|
||||
y 6^
|
||||
z 1v
|
||||
. 9v
|
||||
/ 0v
|
||||
; 0-
|
||||
, 8v
|
||||
a 1-
|
||||
b 5v
|
||||
c 3v
|
||||
d 3-
|
||||
e 3^
|
||||
f 4-
|
||||
g 5-
|
||||
h 6-
|
||||
i 8^
|
||||
j 7-
|
||||
k 8-
|
||||
l 9-
|
||||
m 7v
|
||||
n 6v
|
||||
o 9^
|
||||
p 0^
|
||||
q 1^
|
||||
r 4^
|
||||
s 2-
|
||||
t 5^
|
||||
u 7^
|
||||
v 4v
|
||||
w 2^
|
||||
x 2v
|
||||
y 6^
|
||||
z 1v
|
||||
. 9v
|
||||
/ 0v
|
||||
; 0-
|
||||
, 8v
|
||||
1 1
|
||||
2 2
|
||||
3 3
|
||||
4 4
|
||||
5 5
|
||||
6 6
|
||||
7 7
|
||||
8 8
|
||||
9 9
|
||||
0 0
|
||||
%keyname end
|
||||
%quick begin
|
||||
, ,火米精燈料鄰勞類營
|
||||
|
|
|
@ -17,12 +17,10 @@ let package = Package(
|
|||
.package(path: "../HangarRash_SwiftyCapsLockToggler"),
|
||||
.package(path: "../Jad_BookmarkManager"),
|
||||
.package(path: "../Qwertyyb_ShiftKeyUpChecker"),
|
||||
.package(path: "../vChewing_BrailleSputnik"),
|
||||
.package(path: "../vChewing_CandidateWindow"),
|
||||
.package(path: "../vChewing_OSFrameworkImpl"),
|
||||
.package(path: "../vChewing_CocoaExtension"),
|
||||
.package(path: "../vChewing_Hotenka"),
|
||||
.package(path: "../vChewing_IMKUtils"),
|
||||
.package(path: "../vChewing_KimoDataReader"),
|
||||
.package(path: "../vChewing_LangModelAssembly"),
|
||||
.package(path: "../vChewing_Megrez"),
|
||||
.package(path: "../vChewing_NotifierUI"),
|
||||
|
@ -39,14 +37,12 @@ let package = Package(
|
|||
.target(
|
||||
name: "MainAssembly",
|
||||
dependencies: [
|
||||
.product(name: "BrailleSputnik", package: "vChewing_BrailleSputnik"),
|
||||
.product(name: "BookmarkManager", package: "Jad_BookmarkManager"),
|
||||
.product(name: "CandidateWindow", package: "vChewing_CandidateWindow"),
|
||||
.product(name: "OSFrameworkImpl", package: "vChewing_OSFrameworkImpl"),
|
||||
.product(name: "CocoaExtension", package: "vChewing_CocoaExtension"),
|
||||
.product(name: "FolderMonitor", package: "DanielGalasko_FolderMonitor"),
|
||||
.product(name: "Hotenka", package: "vChewing_Hotenka"),
|
||||
.product(name: "IMKUtils", package: "vChewing_IMKUtils"),
|
||||
.product(name: "KimoDataReader", package: "vChewing_KimoDataReader"),
|
||||
.product(name: "LangModelAssembly", package: "vChewing_LangModelAssembly"),
|
||||
.product(name: "Megrez", package: "vChewing_Megrez"),
|
||||
.product(name: "NotifierUI", package: "vChewing_NotifierUI"),
|
||||
|
@ -60,9 +56,6 @@ let package = Package(
|
|||
.product(name: "TooltipUI", package: "vChewing_TooltipUI"),
|
||||
.product(name: "Uninstaller", package: "vChewing_Uninstaller"),
|
||||
.product(name: "UpdateSputnik", package: "vChewing_UpdateSputnik"),
|
||||
],
|
||||
resources: [
|
||||
.process("Resources/convdict.sqlite"),
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
|
|
|
@ -7,41 +7,23 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import AppKit
|
||||
import Shared
|
||||
import SwiftUI
|
||||
|
||||
@available(macOS 12, *)
|
||||
public class CtlAboutUI: NSWindowController, NSWindowDelegate {
|
||||
public static var shared: CtlAboutUI?
|
||||
private var viewController: NSViewController?
|
||||
var useLegacyView: Bool = false
|
||||
|
||||
public init(forceLegacy: Bool = false) {
|
||||
useLegacyView = forceLegacy
|
||||
let newWindow = NSWindow(
|
||||
contentRect: CGRect(x: 401, y: 295, width: 577, height: 568),
|
||||
styleMask: [.titled, .closable, .miniaturizable],
|
||||
backing: .buffered, defer: true
|
||||
)
|
||||
super.init(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)
|
||||
let newWindow = NSWindow(
|
||||
contentRect: CGRect(x: 401, y: 295, width: 577, height: 568),
|
||||
styleMask: [.titled, .closable, .miniaturizable],
|
||||
backing: .buffered, defer: true
|
||||
)
|
||||
let newInstance = CtlAboutUI(window: newWindow)
|
||||
shared = newInstance
|
||||
}
|
||||
guard let shared = shared, let sharedWindow = shared.window else { return }
|
||||
shared.useLegacyView = forceLegacy
|
||||
sharedWindow.delegate = shared
|
||||
if !sharedWindow.isVisible {
|
||||
shared.windowDidLoad()
|
||||
|
@ -55,28 +37,6 @@ public class CtlAboutUI: NSWindowController, NSWindowDelegate {
|
|||
|
||||
override public func 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?.standardWindowButton(.closeButton)?.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,16 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import AppKit
|
||||
import Shared
|
||||
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, *)
|
||||
extension VwrAboutUI: View {
|
||||
public struct VwrAboutUI: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
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 {
|
||||
GroupBox {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
|
@ -33,7 +31,6 @@ extension VwrAboutUI: View {
|
|||
Text("v\(IMEApp.appMainVersionLabel.joined(separator: " Build ")) - \(IMEApp.appSignedDateLabel)").lineLimit(1)
|
||||
}.fixedSize()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -52,11 +49,10 @@ extension VwrAboutUI: View {
|
|||
HStack(alignment: .top) {
|
||||
Text("i18n:aboutWindow.DISCLAIMER_TEXT")
|
||||
.font(.custom("Tahoma", size: 11))
|
||||
.opacity(0.5)
|
||||
.frame(maxWidth: .infinity)
|
||||
VStack(spacing: 4) {
|
||||
Button {
|
||||
CtlAboutUI.shared?.window?.close()
|
||||
dismiss()
|
||||
} label: {
|
||||
Text("i18n:aboutWindow.OK_BUTTON").frame(width: 114)
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ extension AppDelegate {
|
|||
// 先執行 initUserLangModels() 可以在目標辭典檔案不存在的情況下先行生成空白範本檔案。
|
||||
if PrefMgr.shared.shouldAutoReloadUserDataFiles || forced { LMMgr.initUserLangModels() }
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
|
||||
if #available(macOS 10.15, *) { FileObserveProject.shared.touch() }
|
||||
if PrefMgr.shared.phraseEditorAutoReloadExternalModifications {
|
||||
Broadcaster.shared.eventForReloadingPhraseEditor = .init()
|
||||
}
|
||||
|
@ -67,10 +68,6 @@ public extension AppDelegate {
|
|||
|
||||
SecurityAgentHelper.shared.timer?.fire()
|
||||
|
||||
SpeechSputnik.shared.refreshStatus() // 根據現狀條件決定是否初期化語音引擎。
|
||||
|
||||
CandidateTextService.enableFinalSanityCheck()
|
||||
|
||||
// 一旦發現與使用者半衰模組的觀察行為有關的崩潰標記被開啟:
|
||||
// 如果有開啟 Debug 模式的話,就將既有的半衰記憶資料檔案更名+打上當時的時間戳。
|
||||
// 如果沒有開啟 Debug 模式的話,則將半衰記憶資料直接清空。
|
||||
|
@ -90,7 +87,7 @@ public extension AppDelegate {
|
|||
)
|
||||
}
|
||||
|
||||
LMMgr.connectCoreDB()
|
||||
if !PrefMgr.shared.onlyLoadFactoryLangModelsIfNeeded { LMMgr.loadDataModelsOnAppDelegate() }
|
||||
LMMgr.loadCassetteData()
|
||||
LMMgr.initUserLangModels()
|
||||
folderMonitor.folderDidChange = { [weak self] in
|
||||
|
@ -135,9 +132,12 @@ public extension AppDelegate {
|
|||
NSApp.popup()
|
||||
guard result == NSApplication.ModalResponse.alertFirstButtonReturn else { return }
|
||||
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(
|
||||
selfKill: true, defaultDataFolderPath: LMMgr.dataFolderPath(isDefaultFolder: true)
|
||||
isSudo: false, selfKill: true, defaultDataFolderPath: LMMgr.dataFolderPath(isDefaultFolder: true)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -147,7 +147,7 @@ public extension AppDelegate {
|
|||
guard let currentMemorySizeInBytes = NSApplication.memoryFootprint else { return 0 }
|
||||
let currentMemorySize: Double = (Double(currentMemorySizeInBytes) / 1024 / 1024).rounded(toPlaces: 1)
|
||||
switch currentMemorySize {
|
||||
case 1024...:
|
||||
case 768...:
|
||||
vCLog("WARNING: EXCESSIVE MEMORY FOOTPRINT (\(currentMemorySize)MB).")
|
||||
let msgPackage = UNMutableNotificationContent()
|
||||
msgPackage.title = NSLocalizedString("vChewing", comment: "")
|
||||
|
@ -169,10 +169,4 @@ public extension AppDelegate {
|
|||
}
|
||||
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,12 +7,9 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import Hotenka
|
||||
import Shared
|
||||
|
||||
public enum ChineseConverter {
|
||||
public static let shared = HotenkaChineseConverter(
|
||||
sqliteDir: LMMgr.getBundleDataPath("convdict", ext: "sqlite") ?? ":memory:"
|
||||
)
|
||||
public static let shared = HotenkaChineseConverter(jsonDir: LMMgr.getBundleDataPath("convdict", ext: "json"))
|
||||
|
||||
private static var punctuationConversionTable: [(String, String)] = [
|
||||
("【", "︻"), ("】", "︼"), ("〖", "︗"), ("〗", "︘"), ("〔", "︹"), ("〕", "︺"), ("《", "︽"), ("》", "︾"),
|
||||
|
|
|
@ -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 Carbon
|
||||
import Shared
|
||||
|
||||
// MARK: - Top-level Enums relating to Input Mode and Language Supports.
|
||||
|
||||
|
@ -51,7 +52,7 @@ public enum IMEApp {
|
|||
// MARK: - 輸入法的當前的簡繁體中文模式
|
||||
|
||||
public static var currentInputMode: Shared.InputMode {
|
||||
.init(rawValue: PrefMgr().mostRecentInputMode) ?? .imeModeNULL
|
||||
.init(rawValue: PrefMgr.shared.mostRecentInputMode) ?? .imeModeNULL
|
||||
}
|
||||
|
||||
/// 當前鍵盤是否是 JIS 佈局
|
||||
|
@ -61,10 +62,9 @@ public enum IMEApp {
|
|||
|
||||
/// Fart or Beep?
|
||||
public static func buzz() {
|
||||
let prefs = PrefMgr()
|
||||
if prefs.isDebugModeEnabled {
|
||||
NSSound.buzz(fart: !prefs.shouldNotFartInLieuOfBeep)
|
||||
} else if !prefs.shouldNotFartInLieuOfBeep {
|
||||
if PrefMgr.shared.isDebugModeEnabled {
|
||||
NSSound.buzz(fart: !PrefMgr.shared.shouldNotFartInLieuOfBeep)
|
||||
} else if !PrefMgr.shared.shouldNotFartInLieuOfBeep {
|
||||
NSSound.buzz(fart: true)
|
||||
} else {
|
||||
NSSound.beep()
|
|
@ -33,7 +33,7 @@ import Shared
|
|||
/// 威注音輸入法在「組字區與組音區/組筆區同時為空」、
|
||||
/// 且客體軟體正在準備接收使用者文字輸入行為的時候,會處於空狀態。
|
||||
/// 有時,威注音會利用呼叫空狀態的方式,讓組字區內已經顯示出來的內容遞交出去。
|
||||
/// - **關聯詞語狀態 .ofAssociates**: 逐字選字模式內的關聯詞語輸入狀態。
|
||||
/// - **聯想詞狀態 .ofAssociates**: 逐字選字模式內的聯想詞輸入狀態。
|
||||
/// - **中絕狀態 .ofAbortion**: 與 .ofEmpty() 類似,但會扔掉上一個狀態的內容、
|
||||
/// 不將這些內容遞交給客體應用。該狀態在處理完畢之後會被立刻切換至 .ofEmpty()。
|
||||
/// - **遞交狀態 .ofCommitting**: 該狀態會承載要遞交出去的內容,讓輸入法控制器處理時代為遞交。
|
||||
|
@ -200,8 +200,7 @@ public extension IMEState {
|
|||
case .ofCandidates where cursor != marker: return data.attributedStringMarking(for: session)
|
||||
case .ofCandidates where cursor == marker: break
|
||||
case .ofAssociates: return data.attributedStringPlaceholder(for: session)
|
||||
case .ofSymbolTable where displayedText.isEmpty || node.containsCandidateServices:
|
||||
return data.attributedStringPlaceholder(for: session)
|
||||
case .ofSymbolTable where displayedText.isEmpty: return data.attributedStringPlaceholder(for: session)
|
||||
case .ofSymbolTable where !displayedText.isEmpty: break
|
||||
default: break
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue