Repo // Refactoring the entire LMMgr.
This commit is contained in:
parent
03b4fb683d
commit
e6696d910d
|
@ -12,7 +12,7 @@ import Cocoa
|
|||
public protocol IMEStateProtocol {
|
||||
var type: StateType { get }
|
||||
var data: IMEStateDataProtocol { get set }
|
||||
var candidates: [([String], String)] { get set }
|
||||
var candidates: [(keyArray: [String], value: String)] { get set }
|
||||
var hasComposition: Bool { get }
|
||||
var isCandidateContainer: Bool { get }
|
||||
var displayedText: String { get }
|
||||
|
@ -45,7 +45,7 @@ public protocol IMEStateDataProtocol {
|
|||
var displayTextSegments: [String] { get set }
|
||||
var isFilterable: Bool { get }
|
||||
var isMarkedLengthValid: Bool { get }
|
||||
var candidates: [([String], String)] { get set }
|
||||
var candidates: [(keyArray: [String], value: String)] { get set }
|
||||
var displayedText: String { get set }
|
||||
var displayedTextConverted: String { get }
|
||||
var tooltipBackupForInputting: String { get set }
|
||||
|
@ -54,9 +54,7 @@ public protocol IMEStateDataProtocol {
|
|||
var attributedStringNormal: NSAttributedString { get }
|
||||
var attributedStringMarking: NSAttributedString { get }
|
||||
var attributedStringPlaceholder: NSAttributedString { get }
|
||||
var userPhraseKVPair: (String, String) { get }
|
||||
var userPhraseDumped: String { get }
|
||||
var userPhraseDumpedConverted: String { get }
|
||||
var userPhraseKVPair: (keyArray: [String], value: String) { get }
|
||||
var tooltipColorState: TooltipColorState { get set }
|
||||
mutating func updateTooltipForMarking()
|
||||
}
|
||||
|
|
|
@ -174,7 +174,7 @@ public extension IMEState {
|
|||
return result
|
||||
}
|
||||
|
||||
var candidates: [([String], String)] {
|
||||
var candidates: [(keyArray: [String], value: String)] {
|
||||
get { data.candidates }
|
||||
set { data.candidates = newValue }
|
||||
}
|
||||
|
|
|
@ -78,7 +78,7 @@ public struct IMEStateData: IMEStateDataProtocol {
|
|||
public var markedTargetExists: Bool {
|
||||
let pair = userPhraseKVPair
|
||||
return LMMgr.checkIfUserPhraseExist(
|
||||
userPhrase: pair.1, mode: IMEApp.currentInputMode, key: pair.0
|
||||
userPhrase: pair.value, mode: IMEApp.currentInputMode, keyArray: pair.keyArray
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -90,7 +90,7 @@ public struct IMEStateData: IMEStateDataProtocol {
|
|||
|
||||
public var reading: String = ""
|
||||
public var markedReadings = [String]()
|
||||
public var candidates = [([String], String)]()
|
||||
public var candidates = [(keyArray: [String], value: String)]()
|
||||
public var textToCommit: String = ""
|
||||
public var tooltip: String = ""
|
||||
public var tooltipDuration: Double = 1.0
|
||||
|
@ -197,26 +197,12 @@ public extension IMEStateData {
|
|||
return arrOutput.joined(separator: "\u{A0}")
|
||||
}
|
||||
|
||||
var userPhraseKVPair: (String, String) {
|
||||
let key = markedReadings.joined(separator: InputHandler.keySeparator)
|
||||
var userPhraseKVPair: (keyArray: [String], value: String) {
|
||||
let key = markedReadings
|
||||
let value = displayedText.map(\.description)[markedRange].joined()
|
||||
return (key, value)
|
||||
}
|
||||
|
||||
var userPhraseDumped: String {
|
||||
let pair = userPhraseKVPair
|
||||
let nerfedScore = SessionCtl.areWeNerfing && markedTargetExists ? " -114.514" : ""
|
||||
return "\(pair.1) \(pair.0)\(nerfedScore)"
|
||||
}
|
||||
|
||||
var userPhraseDumpedConverted: String {
|
||||
let pair = userPhraseKVPair
|
||||
let text = ChineseConverter.crossConvert(pair.1)
|
||||
let nerfedScore = SessionCtl.areWeNerfing && markedTargetExists ? " -114.514" : ""
|
||||
let convertedMark = "#𝙃𝙪𝙢𝙖𝙣𝘾𝙝𝙚𝙘𝙠𝙍𝙚𝙦𝙪𝙞𝙧𝙚𝙙"
|
||||
return "\(text) \(pair.0)\(nerfedScore) \(convertedMark)"
|
||||
}
|
||||
|
||||
mutating func updateTooltipForMarking() {
|
||||
var tooltipForMarking: String {
|
||||
let pair = userPhraseKVPair
|
||||
|
|
|
@ -1,909 +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 BookmarkManager
|
||||
import LangModelAssembly
|
||||
import NotifierUI
|
||||
import PhraseEditorUI
|
||||
import Shared
|
||||
import SwiftExtension
|
||||
|
||||
/// 使用者辭典資料預設範例檔案名稱。
|
||||
private let kTemplateNameUserPhrases = "template-userphrases"
|
||||
private let kTemplateNameUserReplacements = "template-replacements"
|
||||
private let kTemplateNameUserFilterList = "template-exclusions"
|
||||
private let kTemplateNameUserSymbolPhrases = "template-usersymbolphrases"
|
||||
private let kTemplateNameUserAssociatesCHS = "template-associatedPhrases-chs"
|
||||
private let kTemplateNameUserAssociatesCHT = "template-associatedPhrases-cht"
|
||||
|
||||
public class LMMgr {
|
||||
public static var shared = LMMgr()
|
||||
public private(set) static var lmCHS = vChewingLM.LMInstantiator(isCHS: true)
|
||||
public private(set) static var lmCHT = vChewingLM.LMInstantiator(isCHS: false)
|
||||
public private(set) static var uomCHS = vChewingLM.LMUserOverride(
|
||||
dataURL: LMMgr.userOverrideModelDataURL(.imeModeCHS))
|
||||
public private(set) static var uomCHT = vChewingLM.LMUserOverride(
|
||||
dataURL: LMMgr.userOverrideModelDataURL(.imeModeCHT))
|
||||
|
||||
public static var currentLM: vChewingLM.LMInstantiator {
|
||||
switch IMEApp.currentInputMode {
|
||||
case .imeModeCHS:
|
||||
return Self.lmCHS
|
||||
case .imeModeCHT:
|
||||
return Self.lmCHT
|
||||
case .imeModeNULL:
|
||||
return .init()
|
||||
}
|
||||
}
|
||||
|
||||
public static var currentUOM: vChewingLM.LMUserOverride {
|
||||
switch IMEApp.currentInputMode {
|
||||
case .imeModeCHS:
|
||||
return Self.uomCHS
|
||||
case .imeModeCHT:
|
||||
return Self.uomCHT
|
||||
case .imeModeNULL:
|
||||
return .init(dataURL: Self.userOverrideModelDataURL(IMEApp.currentInputMode))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Functions reacting directly with language models.
|
||||
|
||||
public static func initUserLangModels() {
|
||||
Self.chkUserLMFilesExist(.imeModeCHT)
|
||||
Self.chkUserLMFilesExist(.imeModeCHS)
|
||||
// LMMgr 的 loadUserPhrases 等函式在自動讀取 dataFolderPath 時,
|
||||
// 如果發現自訂目錄不可用,則會自動抹去自訂目錄設定、改採預設目錄。
|
||||
// 所以這裡不需要特別處理。
|
||||
Self.loadUserPhrasesData()
|
||||
}
|
||||
|
||||
public static func loadCoreLanguageModelFile(
|
||||
filenameSansExtension: String, langModel lm: inout vChewingLM.LMInstantiator
|
||||
) {
|
||||
lm.loadLanguageModel(plist: Self.getDictionaryData(filenameSansExtension))
|
||||
}
|
||||
|
||||
public static func loadDataModelsOnAppDelegate() {
|
||||
let globalQueue = DispatchQueue.global(qos: .default)
|
||||
var showFinishNotification = false
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
globalQueue.async {
|
||||
if !Self.lmCHT.isCNSDataLoaded {
|
||||
Self.lmCHT.loadCNSData(plist: Self.getDictionaryData("data-cns"))
|
||||
}
|
||||
if !Self.lmCHT.isMiscDataLoaded {
|
||||
Self.lmCHT.loadMiscData(plist: Self.getDictionaryData("data-zhuyinwen"))
|
||||
}
|
||||
if !Self.lmCHT.isSymbolDataLoaded {
|
||||
Self.lmCHT.loadSymbolData(plist: Self.getDictionaryData("data-symbols"))
|
||||
}
|
||||
if !Self.lmCHS.isCNSDataLoaded {
|
||||
Self.lmCHS.loadCNSData(plist: Self.getDictionaryData("data-cns"))
|
||||
}
|
||||
if !Self.lmCHS.isMiscDataLoaded {
|
||||
Self.lmCHS.loadMiscData(plist: Self.getDictionaryData("data-zhuyinwen"))
|
||||
}
|
||||
if !Self.lmCHS.isSymbolDataLoaded {
|
||||
Self.lmCHS.loadSymbolData(plist: Self.getDictionaryData("data-symbols"))
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
if !Self.lmCHT.isCoreLMLoaded {
|
||||
showFinishNotification = true
|
||||
Notifier.notify(
|
||||
message: NSLocalizedString("Loading CHT Core Dict...", comment: "")
|
||||
)
|
||||
group.enter()
|
||||
globalQueue.async {
|
||||
loadCoreLanguageModelFile(filenameSansExtension: "data-cht", langModel: &Self.lmCHT)
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
if !Self.lmCHS.isCoreLMLoaded {
|
||||
showFinishNotification = true
|
||||
Notifier.notify(
|
||||
message: NSLocalizedString("Loading CHS Core Dict...", comment: "")
|
||||
)
|
||||
group.enter()
|
||||
globalQueue.async {
|
||||
loadCoreLanguageModelFile(filenameSansExtension: "data-chs", langModel: &Self.lmCHS)
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
if showFinishNotification {
|
||||
Notifier.notify(
|
||||
message: NSLocalizedString("Core Dict loading complete.", comment: "")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func loadDataModel(_ mode: Shared.InputMode) {
|
||||
let globalQueue = DispatchQueue.global(qos: .default)
|
||||
var showFinishNotification = false
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
globalQueue.async {
|
||||
switch mode {
|
||||
case .imeModeCHS:
|
||||
if !Self.lmCHS.isCNSDataLoaded {
|
||||
Self.lmCHS.loadCNSData(plist: Self.getDictionaryData("data-cns"))
|
||||
}
|
||||
if !Self.lmCHS.isMiscDataLoaded {
|
||||
Self.lmCHS.loadMiscData(plist: Self.getDictionaryData("data-zhuyinwen"))
|
||||
}
|
||||
if !Self.lmCHS.isSymbolDataLoaded {
|
||||
Self.lmCHS.loadSymbolData(plist: Self.getDictionaryData("data-symbols"))
|
||||
}
|
||||
case .imeModeCHT:
|
||||
if !Self.lmCHT.isCNSDataLoaded {
|
||||
Self.lmCHT.loadCNSData(plist: Self.getDictionaryData("data-cns"))
|
||||
}
|
||||
if !Self.lmCHT.isMiscDataLoaded {
|
||||
Self.lmCHT.loadMiscData(plist: Self.getDictionaryData("data-zhuyinwen"))
|
||||
}
|
||||
if !Self.lmCHT.isSymbolDataLoaded {
|
||||
Self.lmCHT.loadSymbolData(plist: Self.getDictionaryData("data-symbols"))
|
||||
}
|
||||
default: break
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
switch mode {
|
||||
case .imeModeCHS:
|
||||
if !Self.lmCHS.isCoreLMLoaded {
|
||||
showFinishNotification = true
|
||||
Notifier.notify(
|
||||
message: NSLocalizedString("Loading CHS Core Dict...", comment: "")
|
||||
)
|
||||
group.enter()
|
||||
globalQueue.async {
|
||||
loadCoreLanguageModelFile(filenameSansExtension: "data-chs", langModel: &Self.lmCHS)
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
case .imeModeCHT:
|
||||
if !Self.lmCHT.isCoreLMLoaded {
|
||||
showFinishNotification = true
|
||||
Notifier.notify(
|
||||
message: NSLocalizedString("Loading CHT Core Dict...", comment: "")
|
||||
)
|
||||
group.enter()
|
||||
globalQueue.async {
|
||||
loadCoreLanguageModelFile(filenameSansExtension: "data-cht", langModel: &Self.lmCHT)
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
default: break
|
||||
}
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
if showFinishNotification {
|
||||
Notifier.notify(
|
||||
message: NSLocalizedString("Core Dict loading complete.", comment: "")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func reloadFactoryDictionaryPlists() {
|
||||
FrmRevLookupWindow.reloadData()
|
||||
LMMgr.lmCHS.resetFactoryPlistModels()
|
||||
LMMgr.lmCHT.resetFactoryPlistModels()
|
||||
if PrefMgr.shared.onlyLoadFactoryLangModelsIfNeeded {
|
||||
LMMgr.loadDataModel(IMEApp.currentInputMode)
|
||||
} else {
|
||||
LMMgr.loadDataModelsOnAppDelegate()
|
||||
}
|
||||
}
|
||||
|
||||
/// 載入磁帶資料。
|
||||
/// - Remark: cassettePath() 會在輸入法停用磁帶時直接返回
|
||||
public static func loadCassetteData() {
|
||||
vChewingLM.LMInstantiator.loadCassetteData(path: cassettePath())
|
||||
}
|
||||
|
||||
public static func loadUserPhrasesData(type: vChewingLM.ReplacableUserDataType? = nil) {
|
||||
guard let type = type else {
|
||||
Self.lmCHT.loadUserPhrasesData(
|
||||
path: userDictDataURL(mode: .imeModeCHT, type: .thePhrases).path,
|
||||
filterPath: userDictDataURL(mode: .imeModeCHT, type: .theFilter).path
|
||||
)
|
||||
Self.lmCHS.loadUserPhrasesData(
|
||||
path: userDictDataURL(mode: .imeModeCHS, type: .thePhrases).path,
|
||||
filterPath: userDictDataURL(mode: .imeModeCHS, type: .theFilter).path
|
||||
)
|
||||
Self.lmCHT.loadUserSymbolData(path: userDictDataURL(mode: .imeModeCHT, type: .theSymbols).path)
|
||||
Self.lmCHS.loadUserSymbolData(path: userDictDataURL(mode: .imeModeCHS, type: .theSymbols).path)
|
||||
|
||||
if PrefMgr.shared.associatedPhrasesEnabled { Self.loadUserAssociatesData() }
|
||||
if PrefMgr.shared.phraseReplacementEnabled { Self.loadUserPhraseReplacement() }
|
||||
if PrefMgr.shared.useSCPCTypingMode { Self.loadUserSCPCSequencesData() }
|
||||
|
||||
Self.uomCHT.loadData(fromURL: userOverrideModelDataURL(.imeModeCHT))
|
||||
Self.uomCHS.loadData(fromURL: userOverrideModelDataURL(.imeModeCHS))
|
||||
|
||||
CandidateNode.load(url: Self.userSymbolMenuDataURL())
|
||||
return
|
||||
}
|
||||
switch type {
|
||||
case .thePhrases, .theFilter:
|
||||
Self.lmCHT.loadUserPhrasesData(
|
||||
path: userDictDataURL(mode: .imeModeCHT, type: .thePhrases).path,
|
||||
filterPath: userDictDataURL(mode: .imeModeCHT, type: .theFilter).path
|
||||
)
|
||||
Self.lmCHS.loadUserPhrasesData(
|
||||
path: userDictDataURL(mode: .imeModeCHS, type: .thePhrases).path,
|
||||
filterPath: userDictDataURL(mode: .imeModeCHS, type: .theFilter).path
|
||||
)
|
||||
case .theReplacements:
|
||||
if PrefMgr.shared.phraseReplacementEnabled { Self.loadUserPhraseReplacement() }
|
||||
case .theAssociates:
|
||||
if PrefMgr.shared.associatedPhrasesEnabled { Self.loadUserAssociatesData() }
|
||||
case .theSymbols:
|
||||
Self.lmCHT.loadUserSymbolData(
|
||||
path: Self.userDictDataURL(mode: .imeModeCHT, type: .theSymbols).path
|
||||
)
|
||||
Self.lmCHS.loadUserSymbolData(
|
||||
path: Self.userDictDataURL(mode: .imeModeCHS, type: .theSymbols).path
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public static func loadUserAssociatesData() {
|
||||
Self.lmCHT.loadUserAssociatesData(
|
||||
path: Self.userDictDataURL(mode: .imeModeCHT, type: .theAssociates).path
|
||||
)
|
||||
Self.lmCHS.loadUserAssociatesData(
|
||||
path: Self.userDictDataURL(mode: .imeModeCHS, type: .theAssociates).path
|
||||
)
|
||||
}
|
||||
|
||||
public static func loadUserPhraseReplacement() {
|
||||
Self.lmCHT.loadReplacementsData(
|
||||
path: Self.userDictDataURL(mode: .imeModeCHT, type: .theReplacements).path
|
||||
)
|
||||
Self.lmCHS.loadReplacementsData(
|
||||
path: Self.userDictDataURL(mode: .imeModeCHS, type: .theReplacements).path
|
||||
)
|
||||
}
|
||||
|
||||
public static func loadUserSCPCSequencesData() {
|
||||
Self.lmCHT.loadUserSCPCSequencesData(
|
||||
path: Self.userSCPCSequencesURL(.imeModeCHT).path
|
||||
)
|
||||
Self.lmCHS.loadUserSCPCSequencesData(
|
||||
path: Self.userSCPCSequencesURL(.imeModeCHS).path
|
||||
)
|
||||
}
|
||||
|
||||
public static func checkIfUserPhraseExist(
|
||||
userPhrase: String,
|
||||
mode: Shared.InputMode,
|
||||
key unigramKey: String,
|
||||
factoryDictionaryOnly: Bool = false
|
||||
) -> Bool {
|
||||
switch mode {
|
||||
case .imeModeCHS:
|
||||
return lmCHS.hasKeyValuePairFor(
|
||||
keyArray: [unigramKey], value: userPhrase, factoryDictionaryOnly: factoryDictionaryOnly
|
||||
)
|
||||
case .imeModeCHT:
|
||||
return lmCHT.hasKeyValuePairFor(
|
||||
keyArray: [unigramKey], value: userPhrase, factoryDictionaryOnly: factoryDictionaryOnly
|
||||
)
|
||||
case .imeModeNULL: return false
|
||||
}
|
||||
}
|
||||
|
||||
public static func setPhraseReplacementEnabled(_ state: Bool) {
|
||||
Self.lmCHT.isPhraseReplacementEnabled = state
|
||||
Self.lmCHS.isPhraseReplacementEnabled = state
|
||||
}
|
||||
|
||||
public static func setCNSEnabled(_ state: Bool) {
|
||||
Self.lmCHT.isCNSEnabled = state
|
||||
Self.lmCHS.isCNSEnabled = state
|
||||
}
|
||||
|
||||
public static func setSymbolEnabled(_ state: Bool) {
|
||||
Self.lmCHT.isSymbolEnabled = state
|
||||
Self.lmCHS.isSymbolEnabled = state
|
||||
}
|
||||
|
||||
public static func setSCPCEnabled(_ state: Bool) {
|
||||
Self.lmCHT.isSCPCEnabled = state
|
||||
Self.lmCHS.isSCPCEnabled = state
|
||||
}
|
||||
|
||||
public static func setCassetteEnabled(_ state: Bool) {
|
||||
Self.lmCHT.isCassetteEnabled = state
|
||||
Self.lmCHS.isCassetteEnabled = state
|
||||
}
|
||||
|
||||
public static func setDeltaOfCalendarYears(_ delta: Int) {
|
||||
Self.lmCHT.deltaOfCalendarYears = delta
|
||||
Self.lmCHS.deltaOfCalendarYears = delta
|
||||
}
|
||||
|
||||
// MARK: - 獲取原廠核心語彙檔案資料所在路徑(優先獲取 Containers 下的資料檔案)。
|
||||
|
||||
// 該函式目前僅供步天歌繁簡轉換引擎使用,並不會檢查目標檔案格式的實際可用性。
|
||||
|
||||
public static func getBundleDataPath(_ filenameSansExt: String, factory: Bool = false) -> String {
|
||||
let factory = PrefMgr.shared.useExternalFactoryDict ? factory : true
|
||||
let factoryPath = Bundle.main.path(forResource: filenameSansExt, ofType: "plist")!
|
||||
let containerPath = Self.appSupportURL.appendingPathComponent("vChewingFactoryData/\(filenameSansExt).plist").path
|
||||
.expandingTildeInPath
|
||||
var isFailed = false
|
||||
if !factory {
|
||||
var isFolder = ObjCBool(false)
|
||||
if !FileManager.default.fileExists(atPath: containerPath, isDirectory: &isFolder) { isFailed = true }
|
||||
if !isFailed, !FileManager.default.isReadableFile(atPath: containerPath) { isFailed = true }
|
||||
}
|
||||
let result = (factory || isFailed) ? factoryPath : containerPath
|
||||
return result
|
||||
}
|
||||
|
||||
// MARK: - 獲取原廠核心語彙檔案資料本身(優先獲取 Containers 下的資料檔案),可能會出 nil。
|
||||
|
||||
public static func getDictionaryData(_ filenameSansExt: String, factory: Bool = false) -> (
|
||||
dict: [String: [Data]]?, path: String
|
||||
) {
|
||||
let factory = PrefMgr.shared.useExternalFactoryDict ? factory : true
|
||||
let factoryResultURL = Bundle.main.url(forResource: filenameSansExt, withExtension: "plist")
|
||||
let containerResultURL = Self.appSupportURL.appendingPathComponent("vChewingFactoryData/\(filenameSansExt).plist")
|
||||
var lastReadPath = factoryResultURL?.path ?? "Factory file missing: \(filenameSansExt).plist"
|
||||
|
||||
func getPlistData(url: URL?) -> [String: [Data]]? {
|
||||
var isFailed = false
|
||||
var isFolder = ObjCBool(false)
|
||||
guard let url = url else {
|
||||
vCLog("URL Invalid.")
|
||||
return nil
|
||||
}
|
||||
defer { lastReadPath = url.path }
|
||||
if !FileManager.default.fileExists(atPath: url.path, isDirectory: &isFolder) { isFailed = true }
|
||||
if !isFailed, !FileManager.default.isReadableFile(atPath: url.path) { isFailed = true }
|
||||
if isFailed {
|
||||
vCLog("↑ Exception happened when reading plist file at: \(url.path).")
|
||||
return nil
|
||||
}
|
||||
do {
|
||||
let rawData = try Data(contentsOf: url)
|
||||
return try PropertyListSerialization.propertyList(from: rawData, format: nil) as? [String: [Data]] ?? nil
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
let result =
|
||||
factory
|
||||
? getPlistData(url: factoryResultURL)
|
||||
: getPlistData(url: containerResultURL) ?? getPlistData(url: factoryResultURL)
|
||||
if result == nil {
|
||||
vCLog("↑ Exception happened when reading plist file at: \(lastReadPath).")
|
||||
}
|
||||
return (dict: result, path: lastReadPath)
|
||||
}
|
||||
|
||||
// MARK: - 使用者語彙檔案的具體檔案名稱路徑定義
|
||||
|
||||
// Swift 的 appendingPathComponent 需要藉由 URL 完成。
|
||||
|
||||
/// 指定的使用者辭典資料路徑。
|
||||
/// - Parameters:
|
||||
/// - mode: 繁簡模式。
|
||||
/// - type: 辭典資料類型
|
||||
/// - Returns: 資料路徑(URL)。
|
||||
public static func userDictDataURL(mode: Shared.InputMode, type: vChewingLM.ReplacableUserDataType) -> URL {
|
||||
var fileName: String = {
|
||||
switch type {
|
||||
case .thePhrases: return "userdata"
|
||||
case .theFilter: return "exclude-phrases"
|
||||
case .theReplacements: return "phrases-replacement"
|
||||
case .theAssociates: return "associatedPhrases"
|
||||
case .theSymbols: return "usersymbolphrases"
|
||||
}
|
||||
}()
|
||||
fileName.append((mode == .imeModeCHT) ? "-cht.txt" : "-chs.txt")
|
||||
return URL(fileURLWithPath: dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName)
|
||||
}
|
||||
|
||||
/// 使用者逐字選字模式候選字詞順序資料路徑。
|
||||
/// - Parameter mode: 簡繁體輸入模式。
|
||||
/// - Returns: 資料路徑(URL)。
|
||||
public static func userSCPCSequencesURL(_ mode: Shared.InputMode) -> URL {
|
||||
let fileName = (mode == .imeModeCHT) ? "data-plain-bpmf-cht.plist" : "data-plain-bpmf-chs.plist"
|
||||
return URL(fileURLWithPath: dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName)
|
||||
}
|
||||
|
||||
/// 使用者波浪符號選單資料路徑。
|
||||
/// - Returns: 資料路徑(URL)。
|
||||
public static func userSymbolMenuDataURL() -> URL {
|
||||
let fileName = "symbols.dat"
|
||||
return URL(fileURLWithPath: dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName)
|
||||
}
|
||||
|
||||
/// 使用者半衰記憶模組資料的存取頻次特別高,且資料新陳代謝速度快,所以只適合放在預設的使用者資料目錄下。
|
||||
/// 也就是「~/Library/Application Support/vChewing/」目錄下,且不會隨著使用者辭典目錄的改變而改變。
|
||||
/// - Parameter mode: 簡繁體輸入模式。
|
||||
/// - Returns: 資料路徑(URL)。
|
||||
public static func userOverrideModelDataURL(_ mode: Shared.InputMode) -> URL {
|
||||
let fileName: String = {
|
||||
switch mode {
|
||||
case .imeModeCHS: return "vChewing_override-model-data-chs.dat"
|
||||
case .imeModeCHT: return "vChewing_override-model-data-cht.dat"
|
||||
case .imeModeNULL: return "vChewing_override-model-data-dummy.dat"
|
||||
}
|
||||
}()
|
||||
|
||||
return URL(
|
||||
fileURLWithPath: dataFolderPath(isDefaultFolder: true)
|
||||
).deletingLastPathComponent().appendingPathComponent(fileName)
|
||||
}
|
||||
|
||||
// MARK: - 檢查具體的使用者語彙檔案是否存在
|
||||
|
||||
public static func ensureFileExists(
|
||||
_ fileURL: URL, deployTemplate templateBasename: String = "1145141919810",
|
||||
extension ext: String = "txt"
|
||||
) -> Bool {
|
||||
let filePath = fileURL.path
|
||||
if !FileManager.default.fileExists(atPath: filePath) {
|
||||
let templateURL = Bundle.main.url(forResource: templateBasename, withExtension: ext)
|
||||
var templateData = Data("".utf8)
|
||||
if templateBasename != "" {
|
||||
do {
|
||||
try templateData = Data(contentsOf: templateURL ?? URL(fileURLWithPath: ""))
|
||||
} catch {
|
||||
templateData = Data("".utf8)
|
||||
}
|
||||
do {
|
||||
try templateData.write(to: URL(fileURLWithPath: filePath))
|
||||
} catch {
|
||||
vCLog("Failed to write template data to: \(filePath)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@discardableResult public static func chkUserLMFilesExist(_ mode: Shared.InputMode) -> Bool {
|
||||
if !userDataFolderExists {
|
||||
return false
|
||||
}
|
||||
/// CandidateNode 資料與 UserOverrideModel 半衰模組資料檔案不需要強行確保存在。
|
||||
/// 前者的話,需要該檔案存在的人自己會建立。
|
||||
/// 後者的話,你在敲字時自己就會建立。
|
||||
var failed = false
|
||||
caseCheck: for type in vChewingLM.ReplacableUserDataType.allCases {
|
||||
let templateName = Self.templateName(for: type, mode: mode)
|
||||
if !ensureFileExists(userDictDataURL(mode: mode, type: type), deployTemplate: templateName) {
|
||||
failed = true
|
||||
break caseCheck
|
||||
}
|
||||
}
|
||||
failed = failed || !ensureFileExists(userSCPCSequencesURL(mode))
|
||||
return !failed
|
||||
}
|
||||
|
||||
private static func templateName(for type: vChewingLM.ReplacableUserDataType, mode: Shared.InputMode) -> String {
|
||||
switch type {
|
||||
case .thePhrases: return kTemplateNameUserPhrases
|
||||
case .theFilter: return kTemplateNameUserFilterList
|
||||
case .theReplacements: return kTemplateNameUserReplacements
|
||||
case .theSymbols: return kTemplateNameUserSymbolPhrases
|
||||
case .theAssociates:
|
||||
return mode == .imeModeCHS ? kTemplateNameUserAssociatesCHS : kTemplateNameUserAssociatesCHT
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 使用者語彙檔案專用目錄的合規性檢查
|
||||
|
||||
// 一次性檢查給定的目錄是否存在寫入合規性(僅用於偏好設定檢查等初步檢查場合,不做任何糾偏行為)
|
||||
public static func checkIfSpecifiedUserDataFolderValid(_ folderPath: String?) -> Bool {
|
||||
var isFolder = ObjCBool(false)
|
||||
let folderExist = FileManager.default.fileExists(atPath: folderPath ?? "", isDirectory: &isFolder)
|
||||
// The above "&" mutates the "isFolder" value to the real one received by the "folderExist".
|
||||
|
||||
// 路徑沒有結尾斜槓的話,會導致目錄合規性判定失準。
|
||||
// 出於每個型別每個函式的自我責任原則,這裡多檢查一遍也不壞。
|
||||
var folderPath = folderPath // Convert the incoming constant to a variable.
|
||||
if isFolder.boolValue {
|
||||
folderPath?.ensureTrailingSlash()
|
||||
}
|
||||
let isFolderWritable = FileManager.default.isWritableFile(atPath: folderPath ?? "")
|
||||
// vCLog("mgrLM: Exist: \(folderExist), IsFolder: \(isFolder.boolValue), isWritable: \(isFolderWritable)")
|
||||
if ((folderExist && !isFolder.boolValue) || !folderExist) || !isFolderWritable {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 檢查給定的磁帶目錄是否存在讀入合規性、且是否為指定格式。
|
||||
public static func checkCassettePathValidity(_ cassettePath: String?) -> Bool {
|
||||
var isFolder = ObjCBool(true)
|
||||
let isExist = FileManager.default.fileExists(atPath: cassettePath ?? "", isDirectory: &isFolder)
|
||||
// The above "&" mutates the "isFolder" value to the real one received by the "isExist".
|
||||
let isReadable = FileManager.default.isReadableFile(atPath: cassettePath ?? "")
|
||||
return !isFolder.boolValue && isExist && isReadable
|
||||
}
|
||||
|
||||
// 檢查給定的目錄是否存在寫入合規性、且糾偏,不接受任何傳入變數。
|
||||
public static var userDataFolderExists: Bool {
|
||||
let folderPath = Self.dataFolderPath(isDefaultFolder: false)
|
||||
var isFolder = ObjCBool(false)
|
||||
var folderExist = FileManager.default.fileExists(atPath: folderPath, isDirectory: &isFolder)
|
||||
// The above "&" mutates the "isFolder" value to the real one received by the "folderExist".
|
||||
// 發現目標路徑不是目錄的話:
|
||||
// 如果要找的目標路徑是原廠目標路徑的話,先將這個路徑的所指對象更名、再認為目錄不存在。
|
||||
// 如果要找的目標路徑不是原廠目標路徑的話,則直接報錯。
|
||||
if folderExist, !isFolder.boolValue {
|
||||
do {
|
||||
if dataFolderPath(isDefaultFolder: false)
|
||||
== dataFolderPath(isDefaultFolder: true)
|
||||
{
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "YYYYMMDD-HHMM'Hrs'-ss's'"
|
||||
let dirAlternative = folderPath + formatter.string(from: Date())
|
||||
try FileManager.default.moveItem(atPath: folderPath, toPath: dirAlternative)
|
||||
} else {
|
||||
throw folderPath
|
||||
}
|
||||
} catch {
|
||||
print("Failed to make path available at: \(error)")
|
||||
return false
|
||||
}
|
||||
folderExist = false
|
||||
}
|
||||
if !folderExist {
|
||||
do {
|
||||
try FileManager.default.createDirectory(
|
||||
atPath: folderPath,
|
||||
withIntermediateDirectories: true,
|
||||
attributes: nil
|
||||
)
|
||||
} catch {
|
||||
print("Failed to create folder: \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: - 用以讀取使用者語彙檔案目錄的函式,會自動對 PrefMgr 當中的參數糾偏。
|
||||
|
||||
// 當且僅當 PrefMgr 當中的參數不合規(比如非實在路徑、或者無權限寫入)時,才會糾偏。
|
||||
|
||||
public static let appSupportURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
|
||||
|
||||
public static func dataFolderPath(isDefaultFolder: Bool) -> String {
|
||||
var userDictPathSpecified = PrefMgr.shared.userDataFolderSpecified.expandingTildeInPath
|
||||
var userDictPathDefault =
|
||||
Self.appSupportURL.appendingPathComponent("vChewing").path.expandingTildeInPath
|
||||
|
||||
userDictPathDefault.ensureTrailingSlash()
|
||||
userDictPathSpecified.ensureTrailingSlash()
|
||||
|
||||
if (userDictPathSpecified == userDictPathDefault)
|
||||
|| isDefaultFolder
|
||||
{
|
||||
return userDictPathDefault
|
||||
}
|
||||
if UserDefaults.standard.object(forKey: UserDef.kUserDataFolderSpecified.rawValue) != nil {
|
||||
BookmarkManager.shared.loadBookmarks()
|
||||
if Self.checkIfSpecifiedUserDataFolderValid(userDictPathSpecified) {
|
||||
return userDictPathSpecified
|
||||
}
|
||||
UserDefaults.standard.removeObject(forKey: UserDef.kUserDataFolderSpecified.rawValue)
|
||||
}
|
||||
return userDictPathDefault
|
||||
}
|
||||
|
||||
public static func cassettePath() -> String {
|
||||
let rawCassettePath = PrefMgr.shared.cassettePath
|
||||
if UserDefaults.standard.object(forKey: UserDef.kCassettePath.rawValue) != nil {
|
||||
BookmarkManager.shared.loadBookmarks()
|
||||
if Self.checkCassettePathValidity(rawCassettePath) { return rawCassettePath }
|
||||
UserDefaults.standard.removeObject(forKey: UserDef.kCassettePath.rawValue)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// MARK: - 重設使用者語彙檔案目錄
|
||||
|
||||
public static func resetSpecifiedUserDataFolder() {
|
||||
UserDefaults.standard.removeObject(forKey: UserDef.kUserDataFolderSpecified.rawValue)
|
||||
Self.initUserLangModels()
|
||||
}
|
||||
|
||||
public static func resetCassettePath() {
|
||||
UserDefaults.standard.removeObject(forKey: UserDef.kCassettePath.rawValue)
|
||||
Self.loadCassetteData()
|
||||
}
|
||||
|
||||
// MARK: - 寫入使用者檔案
|
||||
|
||||
public static func writeUserPhrase(
|
||||
_ userPhrase: String, inputMode mode: Shared.InputMode, areWeDeleting: Bool
|
||||
) -> Bool {
|
||||
var userPhraseOutput: String = userPhrase
|
||||
if !chkUserLMFilesExist(.imeModeCHS)
|
||||
|| !chkUserLMFilesExist(.imeModeCHT)
|
||||
{
|
||||
return false
|
||||
}
|
||||
|
||||
let theType: vChewingLM.ReplacableUserDataType = areWeDeleting ? .theFilter : .thePhrases
|
||||
let theURL = userDictDataURL(mode: mode, type: theType)
|
||||
|
||||
let arr = userPhraseOutput.split(separator: " ")
|
||||
var areWeDuplicating = false
|
||||
if arr.count >= 2 {
|
||||
areWeDuplicating = Self.checkIfUserPhraseExist(
|
||||
userPhrase: arr[0].description, mode: mode, key: arr[1].description, factoryDictionaryOnly: true
|
||||
)
|
||||
}
|
||||
|
||||
if areWeDuplicating, !areWeDeleting {
|
||||
// Do not use ASCII characters to comment here.
|
||||
userPhraseOutput += " #𝙾𝚟𝚎𝚛𝚛𝚒𝚍𝚎"
|
||||
}
|
||||
|
||||
if let writeFile = FileHandle(forUpdatingAtPath: theURL.path),
|
||||
let data = userPhraseOutput.data(using: .utf8),
|
||||
let endl = "\n".data(using: .utf8)
|
||||
{
|
||||
writeFile.seekToEndOfFile()
|
||||
writeFile.write(endl)
|
||||
writeFile.write(data)
|
||||
writeFile.write(endl)
|
||||
writeFile.closeFile()
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
// We enforce the format consolidation here, since the pragma header
|
||||
// will let the UserPhraseLM bypasses the consolidating process on load.
|
||||
if !vChewingLM.LMConsolidator.consolidate(path: theURL.path, pragma: false) {
|
||||
return false
|
||||
}
|
||||
|
||||
// The new FolderMonitor module does NOT monitor cases that files are modified
|
||||
// by the current application itself, requiring additional manual loading process here.
|
||||
if #available(macOS 10.15, *) { FileObserveProject.shared.touch() }
|
||||
if PrefMgr.shared.phraseEditorAutoReloadExternalModifications {
|
||||
CtlPrefWindow.shared?.updatePhraseEditor()
|
||||
}
|
||||
loadUserPhrasesData(type: .thePhrases)
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: - 藉由語彙編輯器開啟使用者檔案
|
||||
|
||||
public static func checkIfUserFilesExistBeforeOpening() -> Bool {
|
||||
if !Self.chkUserLMFilesExist(.imeModeCHS)
|
||||
|| !Self.chkUserLMFilesExist(.imeModeCHT)
|
||||
{
|
||||
let content = String(
|
||||
format: NSLocalizedString(
|
||||
"Please check the permission at \"%@\".", comment: ""
|
||||
),
|
||||
Self.dataFolderPath(isDefaultFolder: false)
|
||||
)
|
||||
DispatchQueue.main.async {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Unable to create the user phrase file.", comment: "")
|
||||
alert.informativeText = content
|
||||
alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
|
||||
alert.runModal()
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public static func openUserDictFile(type: vChewingLM.ReplacableUserDataType, dual: Bool = false, alt: Bool) {
|
||||
let app: String = alt ? "" : "Finder"
|
||||
openPhraseFile(fromURL: userDictDataURL(mode: IMEApp.currentInputMode, type: type), app: app)
|
||||
guard dual else { return }
|
||||
openPhraseFile(fromURL: userDictDataURL(mode: IMEApp.currentInputMode.reversed, type: type), app: app)
|
||||
}
|
||||
|
||||
/// 用指定應用開啟指定檔案。
|
||||
/// - Remark: 如果你的 App 有 Sandbox 處理過的話,請勿給 app 傳入 "vim" 參數,因為 Sandbox 會阻止之。
|
||||
/// - Parameters:
|
||||
/// - url: 檔案 URL。
|
||||
/// - app: 指定 App 應用的 binary 檔案名稱。
|
||||
public static func openPhraseFile(fromURL url: URL, app: String = "") {
|
||||
if !Self.checkIfUserFilesExistBeforeOpening() { return }
|
||||
DispatchQueue.main.async {
|
||||
switch app {
|
||||
case "vim":
|
||||
let process = Process()
|
||||
let pipe = Pipe()
|
||||
process.executableURL = URL(fileURLWithPath: "/bin/sh/")
|
||||
process.arguments = ["-c", "open '/usr/bin/vim'", "'\(url.path)'"]
|
||||
process.standardOutput = pipe
|
||||
process.standardError = pipe
|
||||
process.terminationHandler = { process in
|
||||
vCLog("\ndidFinish: \(!process.isRunning)")
|
||||
}
|
||||
let fileHandle = pipe.fileHandleForReading
|
||||
do {
|
||||
try process.run()
|
||||
} catch {
|
||||
NSWorkspace.shared.openFile(url.path, withApplication: "TextEdit")
|
||||
}
|
||||
do {
|
||||
if let theData = try fileHandle.readToEnd(),
|
||||
let outStr = String(data: theData, encoding: .utf8)
|
||||
{
|
||||
vCLog(outStr)
|
||||
}
|
||||
} catch {}
|
||||
case "Finder":
|
||||
NSWorkspace.shared.activateFileViewerSelecting([url])
|
||||
default:
|
||||
if !NSWorkspace.shared.openFile(url.path, withApplication: app) {
|
||||
NSWorkspace.shared.openFile(url.path, withApplication: "TextEdit")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UOM
|
||||
|
||||
public static func saveUserOverrideModelData() {
|
||||
let globalQueue = DispatchQueue.global(qos: .default)
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
globalQueue.async {
|
||||
Self.uomCHT.saveData(toURL: userOverrideModelDataURL(.imeModeCHT))
|
||||
group.leave()
|
||||
}
|
||||
group.enter()
|
||||
globalQueue.async {
|
||||
Self.uomCHS.saveData(toURL: userOverrideModelDataURL(.imeModeCHS))
|
||||
group.leave()
|
||||
}
|
||||
_ = group.wait(timeout: .distantFuture)
|
||||
group.notify(queue: DispatchQueue.main) {}
|
||||
}
|
||||
|
||||
public static func bleachSpecifiedSuggestions(targets: [String], mode: Shared.InputMode) {
|
||||
switch mode {
|
||||
case .imeModeCHS:
|
||||
Self.uomCHT.bleachSpecifiedSuggestions(targets: targets, saveCallback: { Self.uomCHT.saveData() })
|
||||
case .imeModeCHT:
|
||||
Self.uomCHS.bleachSpecifiedSuggestions(targets: targets, saveCallback: { Self.uomCHS.saveData() })
|
||||
case .imeModeNULL:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public static func removeUnigramsFromUserOverrideModel(_ mode: Shared.InputMode) {
|
||||
switch mode {
|
||||
case .imeModeCHS:
|
||||
Self.uomCHT.bleachUnigrams(saveCallback: { Self.uomCHT.saveData() })
|
||||
case .imeModeCHT:
|
||||
Self.uomCHS.bleachUnigrams(saveCallback: { Self.uomCHS.saveData() })
|
||||
case .imeModeNULL:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public static func clearUserOverrideModelData(_ mode: Shared.InputMode = .imeModeNULL) {
|
||||
switch mode {
|
||||
case .imeModeCHS:
|
||||
Self.uomCHS.clearData(withURL: userOverrideModelDataURL(.imeModeCHS))
|
||||
case .imeModeCHT:
|
||||
Self.uomCHT.clearData(withURL: userOverrideModelDataURL(.imeModeCHT))
|
||||
case .imeModeNULL:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension LMMgr: PhraseEditorDelegate {
|
||||
public var currentInputMode: Shared.InputMode { IMEApp.currentInputMode }
|
||||
|
||||
public func openPhraseFile(mode: Shared.InputMode, type: vChewingLM.ReplacableUserDataType, app: String) {
|
||||
Self.openPhraseFile(fromURL: Self.userDictDataURL(mode: mode, type: type), app: app)
|
||||
}
|
||||
|
||||
public func consolidate(text strProcessed: inout String, pragma shouldCheckPragma: Bool) {
|
||||
vChewingLM.LMConsolidator.consolidate(text: &strProcessed, pragma: shouldCheckPragma)
|
||||
}
|
||||
|
||||
public func checkIfUserPhraseExist(userPhrase: String, mode: Shared.InputMode, key unigramKey: String) -> Bool {
|
||||
Self.checkIfUserPhraseExist(userPhrase: userPhrase, mode: mode, key: unigramKey)
|
||||
}
|
||||
|
||||
public func retrieveData(mode: Shared.InputMode, type: vChewingLM.ReplacableUserDataType) -> String {
|
||||
Self.retrieveData(mode: mode, type: type)
|
||||
}
|
||||
|
||||
public static func retrieveData(mode: Shared.InputMode, type: vChewingLM.ReplacableUserDataType) -> String {
|
||||
vCLog("Retrieving data. Mode: \(mode.localizedDescription), type: \(type.localizedDescription)")
|
||||
let theURL = Self.userDictDataURL(mode: mode, type: type)
|
||||
do {
|
||||
return try .init(contentsOf: theURL, encoding: .utf8)
|
||||
} catch {
|
||||
vCLog("Error reading: \(theURL.absoluteString)")
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
public func saveData(mode: Shared.InputMode, type: vChewingLM.ReplacableUserDataType, data: String) -> String {
|
||||
Self.saveData(mode: mode, type: type, data: data)
|
||||
}
|
||||
|
||||
@discardableResult public static func saveData(
|
||||
mode: Shared.InputMode, type: vChewingLM.ReplacableUserDataType, data: String
|
||||
) -> String {
|
||||
DispatchQueue.main.async {
|
||||
let theURL = Self.userDictDataURL(mode: mode, type: type)
|
||||
do {
|
||||
try data.write(to: theURL, atomically: true, encoding: .utf8)
|
||||
Self.loadUserPhrasesData(type: type)
|
||||
} catch {
|
||||
vCLog("Failed to save current database to: \(theURL.absoluteString)")
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
public func tagOverrides(in strProcessed: inout String, mode: Shared.InputMode) {
|
||||
let outputStack: NSMutableString = .init()
|
||||
switch mode {
|
||||
case .imeModeCHT:
|
||||
if !Self.lmCHT.isCoreLMLoaded {
|
||||
Notifier.notify(
|
||||
message: NSLocalizedString("Loading CHT Core Dict...", comment: "")
|
||||
)
|
||||
Self.loadCoreLanguageModelFile(
|
||||
filenameSansExtension: "data-cht", langModel: &Self.lmCHT
|
||||
)
|
||||
Notifier.notify(
|
||||
message: NSLocalizedString("Core Dict loading complete.", comment: "")
|
||||
)
|
||||
}
|
||||
case .imeModeCHS:
|
||||
if !Self.lmCHS.isCoreLMLoaded {
|
||||
Notifier.notify(
|
||||
message: NSLocalizedString("Loading CHS Core Dict...", comment: "")
|
||||
)
|
||||
Self.loadCoreLanguageModelFile(
|
||||
filenameSansExtension: "data-chs", langModel: &Self.lmCHS
|
||||
)
|
||||
Notifier.notify(
|
||||
message: NSLocalizedString("Core Dict loading complete.", comment: "")
|
||||
)
|
||||
}
|
||||
case .imeModeNULL: return
|
||||
}
|
||||
for currentLine in strProcessed.split(separator: "\n") {
|
||||
let arr = currentLine.split(separator: " ")
|
||||
guard arr.count >= 2 else { continue }
|
||||
let exists = Self.checkIfUserPhraseExist(
|
||||
userPhrase: arr[0].description, mode: mode, key: arr[1].description, factoryDictionaryOnly: true
|
||||
)
|
||||
outputStack.append(currentLine.description)
|
||||
let replace = !currentLine.contains(" #𝙾𝚟𝚎𝚛𝚛𝚒𝚍𝚎") && exists
|
||||
if replace { outputStack.append(" #𝙾𝚟𝚎𝚛𝚛𝚒𝚍𝚎") }
|
||||
outputStack.append("\n")
|
||||
}
|
||||
strProcessed = outputStack.description
|
||||
}
|
||||
}
|
|
@ -0,0 +1,377 @@
|
|||
// (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 LangModelAssembly
|
||||
import NotifierUI
|
||||
import Shared
|
||||
import SwiftExtension
|
||||
|
||||
public class LMMgr {
|
||||
public static var shared = LMMgr()
|
||||
public private(set) static var lmCHS = vChewingLM.LMInstantiator(isCHS: true)
|
||||
public private(set) static var lmCHT = vChewingLM.LMInstantiator(isCHS: false)
|
||||
public private(set) static var uomCHS = vChewingLM.LMUserOverride(
|
||||
dataURL: LMMgr.userOverrideModelDataURL(.imeModeCHS))
|
||||
public private(set) static var uomCHT = vChewingLM.LMUserOverride(
|
||||
dataURL: LMMgr.userOverrideModelDataURL(.imeModeCHT))
|
||||
|
||||
public static var currentLM: vChewingLM.LMInstantiator {
|
||||
switch IMEApp.currentInputMode {
|
||||
case .imeModeCHS:
|
||||
return Self.lmCHS
|
||||
case .imeModeCHT:
|
||||
return Self.lmCHT
|
||||
case .imeModeNULL:
|
||||
return .init()
|
||||
}
|
||||
}
|
||||
|
||||
public static var currentUOM: vChewingLM.LMUserOverride {
|
||||
switch IMEApp.currentInputMode {
|
||||
case .imeModeCHS:
|
||||
return Self.uomCHS
|
||||
case .imeModeCHT:
|
||||
return Self.uomCHT
|
||||
case .imeModeNULL:
|
||||
return .init(dataURL: Self.userOverrideModelDataURL(IMEApp.currentInputMode))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Functions reacting directly with language models.
|
||||
|
||||
public static func initUserLangModels() {
|
||||
Self.chkUserLMFilesExist(.imeModeCHT)
|
||||
Self.chkUserLMFilesExist(.imeModeCHS)
|
||||
// LMMgr 的 loadUserPhrases 等函式在自動讀取 dataFolderPath 時,
|
||||
// 如果發現自訂目錄不可用,則會自動抹去自訂目錄設定、改採預設目錄。
|
||||
// 所以這裡不需要特別處理。
|
||||
Self.loadUserPhrasesData()
|
||||
}
|
||||
|
||||
public static func loadCoreLanguageModelFile(
|
||||
filenameSansExtension: String, langModel lm: vChewingLM.LMInstantiator
|
||||
) {
|
||||
lm.loadLanguageModel(plist: Self.getDictionaryData(filenameSansExtension))
|
||||
}
|
||||
|
||||
public static func loadDataModelsOnAppDelegate() {
|
||||
let globalQueue = DispatchQueue.global(qos: .default)
|
||||
var showFinishNotification = false
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
globalQueue.async {
|
||||
if !Self.lmCHT.isCNSDataLoaded {
|
||||
Self.lmCHT.loadCNSData(plist: Self.getDictionaryData("data-cns"))
|
||||
}
|
||||
if !Self.lmCHT.isMiscDataLoaded {
|
||||
Self.lmCHT.loadMiscData(plist: Self.getDictionaryData("data-zhuyinwen"))
|
||||
}
|
||||
if !Self.lmCHT.isSymbolDataLoaded {
|
||||
Self.lmCHT.loadSymbolData(plist: Self.getDictionaryData("data-symbols"))
|
||||
}
|
||||
if !Self.lmCHS.isCNSDataLoaded {
|
||||
Self.lmCHS.loadCNSData(plist: Self.getDictionaryData("data-cns"))
|
||||
}
|
||||
if !Self.lmCHS.isMiscDataLoaded {
|
||||
Self.lmCHS.loadMiscData(plist: Self.getDictionaryData("data-zhuyinwen"))
|
||||
}
|
||||
if !Self.lmCHS.isSymbolDataLoaded {
|
||||
Self.lmCHS.loadSymbolData(plist: Self.getDictionaryData("data-symbols"))
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
if !Self.lmCHT.isCoreLMLoaded {
|
||||
showFinishNotification = true
|
||||
Notifier.notify(
|
||||
message: NSLocalizedString("Loading CHT Core Dict...", comment: "")
|
||||
)
|
||||
group.enter()
|
||||
globalQueue.async {
|
||||
loadCoreLanguageModelFile(filenameSansExtension: "data-cht", langModel: Self.lmCHT)
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
if !Self.lmCHS.isCoreLMLoaded {
|
||||
showFinishNotification = true
|
||||
Notifier.notify(
|
||||
message: NSLocalizedString("Loading CHS Core Dict...", comment: "")
|
||||
)
|
||||
group.enter()
|
||||
globalQueue.async {
|
||||
loadCoreLanguageModelFile(filenameSansExtension: "data-chs", langModel: Self.lmCHS)
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
if showFinishNotification {
|
||||
Notifier.notify(
|
||||
message: NSLocalizedString("Core Dict loading complete.", comment: "")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func loadDataModel(_ mode: Shared.InputMode) {
|
||||
let globalQueue = DispatchQueue.global(qos: .default)
|
||||
var showFinishNotification = false
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
globalQueue.async {
|
||||
switch mode {
|
||||
case .imeModeCHS:
|
||||
if !Self.lmCHS.isCNSDataLoaded {
|
||||
Self.lmCHS.loadCNSData(plist: Self.getDictionaryData("data-cns"))
|
||||
}
|
||||
if !Self.lmCHS.isMiscDataLoaded {
|
||||
Self.lmCHS.loadMiscData(plist: Self.getDictionaryData("data-zhuyinwen"))
|
||||
}
|
||||
if !Self.lmCHS.isSymbolDataLoaded {
|
||||
Self.lmCHS.loadSymbolData(plist: Self.getDictionaryData("data-symbols"))
|
||||
}
|
||||
case .imeModeCHT:
|
||||
if !Self.lmCHT.isCNSDataLoaded {
|
||||
Self.lmCHT.loadCNSData(plist: Self.getDictionaryData("data-cns"))
|
||||
}
|
||||
if !Self.lmCHT.isMiscDataLoaded {
|
||||
Self.lmCHT.loadMiscData(plist: Self.getDictionaryData("data-zhuyinwen"))
|
||||
}
|
||||
if !Self.lmCHT.isSymbolDataLoaded {
|
||||
Self.lmCHT.loadSymbolData(plist: Self.getDictionaryData("data-symbols"))
|
||||
}
|
||||
default: break
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
switch mode {
|
||||
case .imeModeCHS:
|
||||
if !Self.lmCHS.isCoreLMLoaded {
|
||||
showFinishNotification = true
|
||||
Notifier.notify(
|
||||
message: NSLocalizedString("Loading CHS Core Dict...", comment: "")
|
||||
)
|
||||
group.enter()
|
||||
globalQueue.async {
|
||||
loadCoreLanguageModelFile(filenameSansExtension: "data-chs", langModel: Self.lmCHS)
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
case .imeModeCHT:
|
||||
if !Self.lmCHT.isCoreLMLoaded {
|
||||
showFinishNotification = true
|
||||
Notifier.notify(
|
||||
message: NSLocalizedString("Loading CHT Core Dict...", comment: "")
|
||||
)
|
||||
group.enter()
|
||||
globalQueue.async {
|
||||
loadCoreLanguageModelFile(filenameSansExtension: "data-cht", langModel: Self.lmCHT)
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
default: break
|
||||
}
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
if showFinishNotification {
|
||||
Notifier.notify(
|
||||
message: NSLocalizedString("Core Dict loading complete.", comment: "")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func reloadFactoryDictionaryPlists() {
|
||||
FrmRevLookupWindow.reloadData()
|
||||
LMMgr.lmCHS.resetFactoryPlistModels()
|
||||
LMMgr.lmCHT.resetFactoryPlistModels()
|
||||
if PrefMgr.shared.onlyLoadFactoryLangModelsIfNeeded {
|
||||
LMMgr.loadDataModel(IMEApp.currentInputMode)
|
||||
} else {
|
||||
LMMgr.loadDataModelsOnAppDelegate()
|
||||
}
|
||||
}
|
||||
|
||||
/// 載入磁帶資料。
|
||||
/// - Remark: cassettePath() 會在輸入法停用磁帶時直接返回
|
||||
public static func loadCassetteData() {
|
||||
vChewingLM.LMInstantiator.loadCassetteData(path: cassettePath())
|
||||
}
|
||||
|
||||
public static func loadUserPhrasesData(type: vChewingLM.ReplacableUserDataType? = nil) {
|
||||
guard let type = type else {
|
||||
Self.lmCHT.loadUserPhrasesData(
|
||||
path: userDictDataURL(mode: .imeModeCHT, type: .thePhrases).path,
|
||||
filterPath: userDictDataURL(mode: .imeModeCHT, type: .theFilter).path
|
||||
)
|
||||
Self.lmCHS.loadUserPhrasesData(
|
||||
path: userDictDataURL(mode: .imeModeCHS, type: .thePhrases).path,
|
||||
filterPath: userDictDataURL(mode: .imeModeCHS, type: .theFilter).path
|
||||
)
|
||||
Self.lmCHT.loadUserSymbolData(path: userDictDataURL(mode: .imeModeCHT, type: .theSymbols).path)
|
||||
Self.lmCHS.loadUserSymbolData(path: userDictDataURL(mode: .imeModeCHS, type: .theSymbols).path)
|
||||
|
||||
if PrefMgr.shared.associatedPhrasesEnabled { Self.loadUserAssociatesData() }
|
||||
if PrefMgr.shared.phraseReplacementEnabled { Self.loadUserPhraseReplacement() }
|
||||
if PrefMgr.shared.useSCPCTypingMode { Self.loadUserSCPCSequencesData() }
|
||||
|
||||
Self.uomCHT.loadData(fromURL: userOverrideModelDataURL(.imeModeCHT))
|
||||
Self.uomCHS.loadData(fromURL: userOverrideModelDataURL(.imeModeCHS))
|
||||
|
||||
CandidateNode.load(url: Self.userSymbolMenuDataURL())
|
||||
return
|
||||
}
|
||||
switch type {
|
||||
case .thePhrases, .theFilter:
|
||||
Self.lmCHT.loadUserPhrasesData(
|
||||
path: userDictDataURL(mode: .imeModeCHT, type: .thePhrases).path,
|
||||
filterPath: userDictDataURL(mode: .imeModeCHT, type: .theFilter).path
|
||||
)
|
||||
Self.lmCHS.loadUserPhrasesData(
|
||||
path: userDictDataURL(mode: .imeModeCHS, type: .thePhrases).path,
|
||||
filterPath: userDictDataURL(mode: .imeModeCHS, type: .theFilter).path
|
||||
)
|
||||
case .theReplacements:
|
||||
if PrefMgr.shared.phraseReplacementEnabled { Self.loadUserPhraseReplacement() }
|
||||
case .theAssociates:
|
||||
if PrefMgr.shared.associatedPhrasesEnabled { Self.loadUserAssociatesData() }
|
||||
case .theSymbols:
|
||||
Self.lmCHT.loadUserSymbolData(
|
||||
path: Self.userDictDataURL(mode: .imeModeCHT, type: .theSymbols).path
|
||||
)
|
||||
Self.lmCHS.loadUserSymbolData(
|
||||
path: Self.userDictDataURL(mode: .imeModeCHS, type: .theSymbols).path
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public static func loadUserAssociatesData() {
|
||||
Self.lmCHT.loadUserAssociatesData(
|
||||
path: Self.userDictDataURL(mode: .imeModeCHT, type: .theAssociates).path
|
||||
)
|
||||
Self.lmCHS.loadUserAssociatesData(
|
||||
path: Self.userDictDataURL(mode: .imeModeCHS, type: .theAssociates).path
|
||||
)
|
||||
}
|
||||
|
||||
public static func loadUserPhraseReplacement() {
|
||||
Self.lmCHT.loadReplacementsData(
|
||||
path: Self.userDictDataURL(mode: .imeModeCHT, type: .theReplacements).path
|
||||
)
|
||||
Self.lmCHS.loadReplacementsData(
|
||||
path: Self.userDictDataURL(mode: .imeModeCHS, type: .theReplacements).path
|
||||
)
|
||||
}
|
||||
|
||||
public static func loadUserSCPCSequencesData() {
|
||||
Self.lmCHT.loadUserSCPCSequencesData(
|
||||
path: Self.userSCPCSequencesURL(.imeModeCHT).path
|
||||
)
|
||||
Self.lmCHS.loadUserSCPCSequencesData(
|
||||
path: Self.userSCPCSequencesURL(.imeModeCHS).path
|
||||
)
|
||||
}
|
||||
|
||||
public static func checkIfUserPhraseExist(
|
||||
userPhrase: String,
|
||||
mode: Shared.InputMode,
|
||||
keyArray: [String],
|
||||
factoryDictionaryOnly: Bool = false
|
||||
) -> Bool {
|
||||
switch mode {
|
||||
case .imeModeCHS:
|
||||
return lmCHS.hasKeyValuePairFor(
|
||||
keyArray: keyArray, value: userPhrase, factoryDictionaryOnly: factoryDictionaryOnly
|
||||
)
|
||||
case .imeModeCHT:
|
||||
return lmCHT.hasKeyValuePairFor(
|
||||
keyArray: keyArray, value: userPhrase, factoryDictionaryOnly: factoryDictionaryOnly
|
||||
)
|
||||
case .imeModeNULL: return false
|
||||
}
|
||||
}
|
||||
|
||||
public static func setPhraseReplacementEnabled(_ state: Bool) {
|
||||
Self.lmCHT.isPhraseReplacementEnabled = state
|
||||
Self.lmCHS.isPhraseReplacementEnabled = state
|
||||
}
|
||||
|
||||
public static func setCNSEnabled(_ state: Bool) {
|
||||
Self.lmCHT.isCNSEnabled = state
|
||||
Self.lmCHS.isCNSEnabled = state
|
||||
}
|
||||
|
||||
public static func setSymbolEnabled(_ state: Bool) {
|
||||
Self.lmCHT.isSymbolEnabled = state
|
||||
Self.lmCHS.isSymbolEnabled = state
|
||||
}
|
||||
|
||||
public static func setSCPCEnabled(_ state: Bool) {
|
||||
Self.lmCHT.isSCPCEnabled = state
|
||||
Self.lmCHS.isSCPCEnabled = state
|
||||
}
|
||||
|
||||
public static func setCassetteEnabled(_ state: Bool) {
|
||||
Self.lmCHT.isCassetteEnabled = state
|
||||
Self.lmCHS.isCassetteEnabled = state
|
||||
}
|
||||
|
||||
public static func setDeltaOfCalendarYears(_ delta: Int) {
|
||||
Self.lmCHT.deltaOfCalendarYears = delta
|
||||
Self.lmCHS.deltaOfCalendarYears = delta
|
||||
}
|
||||
|
||||
// MARK: UOM
|
||||
|
||||
public static func saveUserOverrideModelData() {
|
||||
let globalQueue = DispatchQueue.global(qos: .default)
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
globalQueue.async {
|
||||
Self.uomCHT.saveData(toURL: userOverrideModelDataURL(.imeModeCHT))
|
||||
group.leave()
|
||||
}
|
||||
group.enter()
|
||||
globalQueue.async {
|
||||
Self.uomCHS.saveData(toURL: userOverrideModelDataURL(.imeModeCHS))
|
||||
group.leave()
|
||||
}
|
||||
_ = group.wait(timeout: .distantFuture)
|
||||
group.notify(queue: DispatchQueue.main) {}
|
||||
}
|
||||
|
||||
public static func bleachSpecifiedSuggestions(targets: [String], mode: Shared.InputMode) {
|
||||
switch mode {
|
||||
case .imeModeCHS:
|
||||
Self.uomCHT.bleachSpecifiedSuggestions(targets: targets, saveCallback: { Self.uomCHT.saveData() })
|
||||
case .imeModeCHT:
|
||||
Self.uomCHS.bleachSpecifiedSuggestions(targets: targets, saveCallback: { Self.uomCHS.saveData() })
|
||||
case .imeModeNULL:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public static func removeUnigramsFromUserOverrideModel(_ mode: Shared.InputMode) {
|
||||
switch mode {
|
||||
case .imeModeCHS:
|
||||
Self.uomCHT.bleachUnigrams(saveCallback: { Self.uomCHT.saveData() })
|
||||
case .imeModeCHT:
|
||||
Self.uomCHS.bleachUnigrams(saveCallback: { Self.uomCHS.saveData() })
|
||||
case .imeModeNULL:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public static func clearUserOverrideModelData(_ mode: Shared.InputMode = .imeModeNULL) {
|
||||
switch mode {
|
||||
case .imeModeCHS:
|
||||
Self.uomCHS.clearData(withURL: userOverrideModelDataURL(.imeModeCHS))
|
||||
case .imeModeCHT:
|
||||
Self.uomCHT.clearData(withURL: userOverrideModelDataURL(.imeModeCHT))
|
||||
case .imeModeNULL:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
// (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 LangModelAssembly
|
||||
import NotifierUI
|
||||
import PhraseEditorUI
|
||||
import Shared
|
||||
|
||||
// MARK: - Implements Conforming to Phrase Editor Delegate Protocol
|
||||
|
||||
extension LMMgr: PhraseEditorDelegate {
|
||||
public var currentInputMode: Shared.InputMode { IMEApp.currentInputMode }
|
||||
|
||||
public func openPhraseFile(mode: Shared.InputMode, type: vChewingLM.ReplacableUserDataType, app: String) {
|
||||
Self.openPhraseFile(fromURL: Self.userDictDataURL(mode: mode, type: type), app: app)
|
||||
}
|
||||
|
||||
public func consolidate(text strProcessed: inout String, pragma shouldCheckPragma: Bool) {
|
||||
vChewingLM.LMConsolidator.consolidate(text: &strProcessed, pragma: shouldCheckPragma)
|
||||
}
|
||||
|
||||
public func checkIfUserPhraseExist(userPhrase: String, mode: Shared.InputMode, key unigramKey: String) -> Bool {
|
||||
Self.checkIfUserPhraseExist(userPhrase: userPhrase, mode: mode, keyArray: [unigramKey])
|
||||
}
|
||||
|
||||
public func retrieveData(mode: Shared.InputMode, type: vChewingLM.ReplacableUserDataType) -> String {
|
||||
Self.retrieveData(mode: mode, type: type)
|
||||
}
|
||||
|
||||
public static func retrieveData(mode: Shared.InputMode, type: vChewingLM.ReplacableUserDataType) -> String {
|
||||
vCLog("Retrieving data. Mode: \(mode.localizedDescription), type: \(type.localizedDescription)")
|
||||
let theURL = Self.userDictDataURL(mode: mode, type: type)
|
||||
do {
|
||||
return try .init(contentsOf: theURL, encoding: .utf8)
|
||||
} catch {
|
||||
vCLog("Error reading: \(theURL.absoluteString)")
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
public func saveData(mode: Shared.InputMode, type: vChewingLM.ReplacableUserDataType, data: String) -> String {
|
||||
Self.saveData(mode: mode, type: type, data: data)
|
||||
}
|
||||
|
||||
@discardableResult public static func saveData(
|
||||
mode: Shared.InputMode, type: vChewingLM.ReplacableUserDataType, data: String
|
||||
) -> String {
|
||||
DispatchQueue.main.async {
|
||||
let theURL = Self.userDictDataURL(mode: mode, type: type)
|
||||
do {
|
||||
try data.write(to: theURL, atomically: true, encoding: .utf8)
|
||||
Self.loadUserPhrasesData(type: type)
|
||||
} catch {
|
||||
vCLog("Failed to save current database to: \(theURL.absoluteString)")
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
public func tagOverrides(in strProcessed: inout String, mode: Shared.InputMode) {
|
||||
let outputStack: NSMutableString = .init()
|
||||
switch mode {
|
||||
case .imeModeCHT:
|
||||
if !Self.lmCHT.isCoreLMLoaded {
|
||||
Notifier.notify(
|
||||
message: NSLocalizedString("Loading CHT Core Dict...", comment: "")
|
||||
)
|
||||
Self.loadCoreLanguageModelFile(
|
||||
filenameSansExtension: "data-cht", langModel: Self.lmCHT
|
||||
)
|
||||
Notifier.notify(
|
||||
message: NSLocalizedString("Core Dict loading complete.", comment: "")
|
||||
)
|
||||
}
|
||||
case .imeModeCHS:
|
||||
if !Self.lmCHS.isCoreLMLoaded {
|
||||
Notifier.notify(
|
||||
message: NSLocalizedString("Loading CHS Core Dict...", comment: "")
|
||||
)
|
||||
Self.loadCoreLanguageModelFile(
|
||||
filenameSansExtension: "data-chs", langModel: Self.lmCHS
|
||||
)
|
||||
Notifier.notify(
|
||||
message: NSLocalizedString("Core Dict loading complete.", comment: "")
|
||||
)
|
||||
}
|
||||
case .imeModeNULL: return
|
||||
}
|
||||
for currentLine in strProcessed.split(separator: "\n") {
|
||||
let arr = currentLine.split(separator: " ")
|
||||
guard arr.count >= 2 else { continue }
|
||||
let exists = Self.checkIfUserPhraseExist(
|
||||
userPhrase: arr[0].description, mode: mode,
|
||||
keyArray: arr[1].map(\.description), factoryDictionaryOnly: true
|
||||
)
|
||||
outputStack.append(currentLine.description)
|
||||
let replace = !currentLine.contains(" #𝙾𝚟𝚎𝚛𝚛𝚒𝚍𝚎") && exists
|
||||
if replace { outputStack.append(" #𝙾𝚟𝚎𝚛𝚛𝚒𝚍𝚎") }
|
||||
outputStack.append("\n")
|
||||
}
|
||||
strProcessed = outputStack.description
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
// (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 LangModelAssembly
|
||||
import Shared
|
||||
|
||||
// MARK: - 使用者語彙類型定義
|
||||
|
||||
public extension LMMgr {
|
||||
struct UserPhrase {
|
||||
public private(set) var keyArray: [String]
|
||||
public private(set) var value: String
|
||||
public private(set) var inputMode: Shared.InputMode
|
||||
public private(set) var isConverted: Bool = false
|
||||
public var weight: Double?
|
||||
|
||||
private var isDuplicated: Bool {
|
||||
LMMgr.checkIfUserPhraseExist(userPhrase: value, mode: inputMode, keyArray: keyArray)
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
var result = [String]()
|
||||
result.append(value)
|
||||
result.append(keyArray.joined(separator: "-"))
|
||||
if let weight = weight {
|
||||
result.append(weight.description)
|
||||
}
|
||||
if isDuplicated {
|
||||
result.append("#𝙾𝚟𝚎𝚛𝚛𝚒𝚍𝚎")
|
||||
}
|
||||
if isConverted {
|
||||
result.append("#𝙃𝙪𝙢𝙖𝙣𝘾𝙝𝙚𝙘𝙠𝙍𝙚𝙦𝙪𝙞𝙧𝙚𝙙")
|
||||
}
|
||||
return result.joined(separator: " ")
|
||||
}
|
||||
|
||||
public var crossConverted: UserPhrase {
|
||||
if isConverted { return self }
|
||||
var result = self
|
||||
result.value = ChineseConverter.crossConvert(value)
|
||||
result.inputMode = inputMode.reversed
|
||||
result.isConverted = true
|
||||
return result
|
||||
}
|
||||
|
||||
public func write(toFilter: Bool) -> Bool {
|
||||
guard LMMgr.chkUserLMFilesExist(inputMode) else { return false }
|
||||
|
||||
let theType: vChewingLM.ReplacableUserDataType = toFilter ? .theFilter : .thePhrases
|
||||
let theURL = LMMgr.userDictDataURL(mode: inputMode, type: theType)
|
||||
|
||||
if let writeFile = FileHandle(forUpdatingAtPath: theURL.path),
|
||||
let data = "\n\(description)\n".data(using: .utf8)
|
||||
{
|
||||
writeFile.seekToEndOfFile()
|
||||
writeFile.write(data)
|
||||
writeFile.closeFile()
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
// We enforce the format consolidation here, since the pragma header
|
||||
// will let the UserPhraseLM bypasses the consolidating process on load.
|
||||
if !vChewingLM.LMConsolidator.consolidate(path: theURL.path, pragma: false) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,417 @@
|
|||
// (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 BookmarkManager
|
||||
import LangModelAssembly
|
||||
import Shared
|
||||
import SwiftExtension
|
||||
|
||||
/// 使用者辭典資料預設範例檔案名稱。
|
||||
private let kTemplateNameUserPhrases = "template-userphrases"
|
||||
private let kTemplateNameUserReplacements = "template-replacements"
|
||||
private let kTemplateNameUserFilterList = "template-exclusions"
|
||||
private let kTemplateNameUserSymbolPhrases = "template-usersymbolphrases"
|
||||
private let kTemplateNameUserAssociatesCHS = "template-associatedPhrases-chs"
|
||||
private let kTemplateNameUserAssociatesCHT = "template-associatedPhrases-cht"
|
||||
|
||||
public extension LMMgr {
|
||||
// MARK: - 獲取原廠核心語彙檔案資料所在路徑(優先獲取 Containers 下的資料檔案)。
|
||||
|
||||
// 該函式目前僅供步天歌繁簡轉換引擎使用,並不會檢查目標檔案格式的實際可用性。
|
||||
|
||||
static func getBundleDataPath(_ filenameSansExt: String, factory: Bool = false) -> String {
|
||||
let factory = PrefMgr.shared.useExternalFactoryDict ? factory : true
|
||||
let factoryPath = Bundle.main.path(forResource: filenameSansExt, ofType: "plist")!
|
||||
let containerPath = Self.appSupportURL.appendingPathComponent("vChewingFactoryData/\(filenameSansExt).plist").path
|
||||
.expandingTildeInPath
|
||||
var isFailed = false
|
||||
if !factory {
|
||||
var isFolder = ObjCBool(false)
|
||||
if !FileManager.default.fileExists(atPath: containerPath, isDirectory: &isFolder) { isFailed = true }
|
||||
if !isFailed, !FileManager.default.isReadableFile(atPath: containerPath) { isFailed = true }
|
||||
}
|
||||
let result = (factory || isFailed) ? factoryPath : containerPath
|
||||
return result
|
||||
}
|
||||
|
||||
// MARK: - 獲取原廠核心語彙檔案資料本身(優先獲取 Containers 下的資料檔案),可能會出 nil。
|
||||
|
||||
static func getDictionaryData(_ filenameSansExt: String, factory: Bool = false) -> (
|
||||
dict: [String: [Data]]?, path: String
|
||||
) {
|
||||
let factory = PrefMgr.shared.useExternalFactoryDict ? factory : true
|
||||
let factoryResultURL = Bundle.main.url(forResource: filenameSansExt, withExtension: "plist")
|
||||
let containerResultURL = Self.appSupportURL.appendingPathComponent("vChewingFactoryData/\(filenameSansExt).plist")
|
||||
var lastReadPath = factoryResultURL?.path ?? "Factory file missing: \(filenameSansExt).plist"
|
||||
|
||||
func getPlistData(url: URL?) -> [String: [Data]]? {
|
||||
var isFailed = false
|
||||
var isFolder = ObjCBool(false)
|
||||
guard let url = url else {
|
||||
vCLog("URL Invalid.")
|
||||
return nil
|
||||
}
|
||||
defer { lastReadPath = url.path }
|
||||
if !FileManager.default.fileExists(atPath: url.path, isDirectory: &isFolder) { isFailed = true }
|
||||
if !isFailed, !FileManager.default.isReadableFile(atPath: url.path) { isFailed = true }
|
||||
if isFailed {
|
||||
vCLog("↑ Exception happened when reading plist file at: \(url.path).")
|
||||
return nil
|
||||
}
|
||||
do {
|
||||
let rawData = try Data(contentsOf: url)
|
||||
return try PropertyListSerialization.propertyList(from: rawData, format: nil) as? [String: [Data]] ?? nil
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
let result =
|
||||
factory
|
||||
? getPlistData(url: factoryResultURL)
|
||||
: getPlistData(url: containerResultURL) ?? getPlistData(url: factoryResultURL)
|
||||
if result == nil {
|
||||
vCLog("↑ Exception happened when reading plist file at: \(lastReadPath).")
|
||||
}
|
||||
return (dict: result, path: lastReadPath)
|
||||
}
|
||||
|
||||
// MARK: - 使用者語彙檔案的具體檔案名稱路徑定義
|
||||
|
||||
// Swift 的 appendingPathComponent 需要藉由 URL 完成。
|
||||
|
||||
/// 指定的使用者辭典資料路徑。
|
||||
/// - Parameters:
|
||||
/// - mode: 繁簡模式。
|
||||
/// - type: 辭典資料類型
|
||||
/// - Returns: 資料路徑(URL)。
|
||||
static func userDictDataURL(mode: Shared.InputMode, type: vChewingLM.ReplacableUserDataType) -> URL {
|
||||
var fileName: String = {
|
||||
switch type {
|
||||
case .thePhrases: return "userdata"
|
||||
case .theFilter: return "exclude-phrases"
|
||||
case .theReplacements: return "phrases-replacement"
|
||||
case .theAssociates: return "associatedPhrases"
|
||||
case .theSymbols: return "usersymbolphrases"
|
||||
}
|
||||
}()
|
||||
fileName.append((mode == .imeModeCHT) ? "-cht.txt" : "-chs.txt")
|
||||
return URL(fileURLWithPath: dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName)
|
||||
}
|
||||
|
||||
/// 使用者逐字選字模式候選字詞順序資料路徑。
|
||||
/// - Parameter mode: 簡繁體輸入模式。
|
||||
/// - Returns: 資料路徑(URL)。
|
||||
static func userSCPCSequencesURL(_ mode: Shared.InputMode) -> URL {
|
||||
let fileName = (mode == .imeModeCHT) ? "data-plain-bpmf-cht.plist" : "data-plain-bpmf-chs.plist"
|
||||
return URL(fileURLWithPath: dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName)
|
||||
}
|
||||
|
||||
/// 使用者波浪符號選單資料路徑。
|
||||
/// - Returns: 資料路徑(URL)。
|
||||
static func userSymbolMenuDataURL() -> URL {
|
||||
let fileName = "symbols.dat"
|
||||
return URL(fileURLWithPath: dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName)
|
||||
}
|
||||
|
||||
/// 使用者半衰記憶模組資料的存取頻次特別高,且資料新陳代謝速度快,所以只適合放在預設的使用者資料目錄下。
|
||||
/// 也就是「~/Library/Application Support/vChewing/」目錄下,且不會隨著使用者辭典目錄的改變而改變。
|
||||
/// - Parameter mode: 簡繁體輸入模式。
|
||||
/// - Returns: 資料路徑(URL)。
|
||||
static func userOverrideModelDataURL(_ mode: Shared.InputMode) -> URL {
|
||||
let fileName: String = {
|
||||
switch mode {
|
||||
case .imeModeCHS: return "vChewing_override-model-data-chs.dat"
|
||||
case .imeModeCHT: return "vChewing_override-model-data-cht.dat"
|
||||
case .imeModeNULL: return "vChewing_override-model-data-dummy.dat"
|
||||
}
|
||||
}()
|
||||
|
||||
return URL(
|
||||
fileURLWithPath: dataFolderPath(isDefaultFolder: true)
|
||||
).deletingLastPathComponent().appendingPathComponent(fileName)
|
||||
}
|
||||
|
||||
// MARK: - 使用者語彙檔案專用目錄的合規性檢查
|
||||
|
||||
// 一次性檢查給定的目錄是否存在寫入合規性(僅用於偏好設定檢查等初步檢查場合,不做任何糾偏行為)
|
||||
static func checkIfSpecifiedUserDataFolderValid(_ folderPath: String?) -> Bool {
|
||||
var isFolder = ObjCBool(false)
|
||||
let folderExist = FileManager.default.fileExists(atPath: folderPath ?? "", isDirectory: &isFolder)
|
||||
// The above "&" mutates the "isFolder" value to the real one received by the "folderExist".
|
||||
|
||||
// 路徑沒有結尾斜槓的話,會導致目錄合規性判定失準。
|
||||
// 出於每個型別每個函式的自我責任原則,這裡多檢查一遍也不壞。
|
||||
var folderPath = folderPath // Convert the incoming constant to a variable.
|
||||
if isFolder.boolValue {
|
||||
folderPath?.ensureTrailingSlash()
|
||||
}
|
||||
let isFolderWritable = FileManager.default.isWritableFile(atPath: folderPath ?? "")
|
||||
// vCLog("mgrLM: Exist: \(folderExist), IsFolder: \(isFolder.boolValue), isWritable: \(isFolderWritable)")
|
||||
if ((folderExist && !isFolder.boolValue) || !folderExist) || !isFolderWritable {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 檢查給定的磁帶目錄是否存在讀入合規性、且是否為指定格式。
|
||||
static func checkCassettePathValidity(_ cassettePath: String?) -> Bool {
|
||||
var isFolder = ObjCBool(true)
|
||||
let isExist = FileManager.default.fileExists(atPath: cassettePath ?? "", isDirectory: &isFolder)
|
||||
// The above "&" mutates the "isFolder" value to the real one received by the "isExist".
|
||||
let isReadable = FileManager.default.isReadableFile(atPath: cassettePath ?? "")
|
||||
return !isFolder.boolValue && isExist && isReadable
|
||||
}
|
||||
|
||||
// 檢查給定的目錄是否存在寫入合規性、且糾偏,不接受任何傳入變數。
|
||||
static var userDataFolderExists: Bool {
|
||||
let folderPath = Self.dataFolderPath(isDefaultFolder: false)
|
||||
var isFolder = ObjCBool(false)
|
||||
var folderExist = FileManager.default.fileExists(atPath: folderPath, isDirectory: &isFolder)
|
||||
// The above "&" mutates the "isFolder" value to the real one received by the "folderExist".
|
||||
// 發現目標路徑不是目錄的話:
|
||||
// 如果要找的目標路徑是原廠目標路徑的話,先將這個路徑的所指對象更名、再認為目錄不存在。
|
||||
// 如果要找的目標路徑不是原廠目標路徑的話,則直接報錯。
|
||||
if folderExist, !isFolder.boolValue {
|
||||
do {
|
||||
if dataFolderPath(isDefaultFolder: false)
|
||||
== dataFolderPath(isDefaultFolder: true)
|
||||
{
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "YYYYMMDD-HHMM'Hrs'-ss's'"
|
||||
let dirAlternative = folderPath + formatter.string(from: Date())
|
||||
try FileManager.default.moveItem(atPath: folderPath, toPath: dirAlternative)
|
||||
} else {
|
||||
throw folderPath
|
||||
}
|
||||
} catch {
|
||||
print("Failed to make path available at: \(error)")
|
||||
return false
|
||||
}
|
||||
folderExist = false
|
||||
}
|
||||
if !folderExist {
|
||||
do {
|
||||
try FileManager.default.createDirectory(
|
||||
atPath: folderPath,
|
||||
withIntermediateDirectories: true,
|
||||
attributes: nil
|
||||
)
|
||||
} catch {
|
||||
print("Failed to create folder: \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: - 用以讀取使用者語彙檔案目錄的函式,會自動對 PrefMgr 當中的參數糾偏。
|
||||
|
||||
// 當且僅當 PrefMgr 當中的參數不合規(比如非實在路徑、或者無權限寫入)時,才會糾偏。
|
||||
|
||||
static let appSupportURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
|
||||
|
||||
static func dataFolderPath(isDefaultFolder: Bool) -> String {
|
||||
var userDictPathSpecified = PrefMgr.shared.userDataFolderSpecified.expandingTildeInPath
|
||||
var userDictPathDefault =
|
||||
Self.appSupportURL.appendingPathComponent("vChewing").path.expandingTildeInPath
|
||||
|
||||
userDictPathDefault.ensureTrailingSlash()
|
||||
userDictPathSpecified.ensureTrailingSlash()
|
||||
|
||||
if (userDictPathSpecified == userDictPathDefault)
|
||||
|| isDefaultFolder
|
||||
{
|
||||
return userDictPathDefault
|
||||
}
|
||||
if UserDefaults.standard.object(forKey: UserDef.kUserDataFolderSpecified.rawValue) != nil {
|
||||
BookmarkManager.shared.loadBookmarks()
|
||||
if Self.checkIfSpecifiedUserDataFolderValid(userDictPathSpecified) {
|
||||
return userDictPathSpecified
|
||||
}
|
||||
UserDefaults.standard.removeObject(forKey: UserDef.kUserDataFolderSpecified.rawValue)
|
||||
}
|
||||
return userDictPathDefault
|
||||
}
|
||||
|
||||
static func cassettePath() -> String {
|
||||
let rawCassettePath = PrefMgr.shared.cassettePath
|
||||
if UserDefaults.standard.object(forKey: UserDef.kCassettePath.rawValue) != nil {
|
||||
BookmarkManager.shared.loadBookmarks()
|
||||
if Self.checkCassettePathValidity(rawCassettePath) { return rawCassettePath }
|
||||
UserDefaults.standard.removeObject(forKey: UserDef.kCassettePath.rawValue)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// MARK: - 重設使用者語彙檔案目錄
|
||||
|
||||
static func resetSpecifiedUserDataFolder() {
|
||||
UserDefaults.standard.removeObject(forKey: UserDef.kUserDataFolderSpecified.rawValue)
|
||||
Self.initUserLangModels()
|
||||
}
|
||||
|
||||
static func resetCassettePath() {
|
||||
UserDefaults.standard.removeObject(forKey: UserDef.kCassettePath.rawValue)
|
||||
Self.loadCassetteData()
|
||||
}
|
||||
|
||||
// MARK: - 寫入使用者檔案
|
||||
|
||||
static func writeUserPhrasesAtOnce(
|
||||
_ userPhrase: UserPhrase, areWeFiltering: Bool,
|
||||
errorHandler: (() -> Void)? = nil
|
||||
) {
|
||||
let resultA = userPhrase.write(toFilter: areWeFiltering)
|
||||
let resultB = userPhrase.crossConverted.write(toFilter: areWeFiltering)
|
||||
guard resultA, resultB else {
|
||||
if let errorHandler = errorHandler {
|
||||
errorHandler()
|
||||
}
|
||||
return
|
||||
}
|
||||
// The new FolderMonitor module does NOT monitor cases that files are modified
|
||||
// by the current application itself, requiring additional manual loading process here.
|
||||
if #available(macOS 10.15, *) { FileObserveProject.shared.touch() }
|
||||
if PrefMgr.shared.phraseEditorAutoReloadExternalModifications {
|
||||
CtlPrefWindow.shared?.updatePhraseEditor()
|
||||
}
|
||||
loadUserPhrasesData(type: .thePhrases)
|
||||
}
|
||||
|
||||
// MARK: - 藉由語彙編輯器開啟使用者檔案
|
||||
|
||||
private static func checkIfUserFilesExistBeforeOpening() -> Bool {
|
||||
if !Self.chkUserLMFilesExist(.imeModeCHS)
|
||||
|| !Self.chkUserLMFilesExist(.imeModeCHT)
|
||||
{
|
||||
let content = String(
|
||||
format: NSLocalizedString(
|
||||
"Please check the permission at \"%@\".", comment: ""
|
||||
),
|
||||
Self.dataFolderPath(isDefaultFolder: false)
|
||||
)
|
||||
DispatchQueue.main.async {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Unable to create the user phrase file.", comment: "")
|
||||
alert.informativeText = content
|
||||
alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
|
||||
alert.runModal()
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static func openUserDictFile(type: vChewingLM.ReplacableUserDataType, dual: Bool = false, alt: Bool) {
|
||||
let app: String = alt ? "" : "Finder"
|
||||
openPhraseFile(fromURL: userDictDataURL(mode: IMEApp.currentInputMode, type: type), app: app)
|
||||
guard dual else { return }
|
||||
openPhraseFile(fromURL: userDictDataURL(mode: IMEApp.currentInputMode.reversed, type: type), app: app)
|
||||
}
|
||||
|
||||
/// 用指定應用開啟指定檔案。
|
||||
/// - Remark: 如果你的 App 有 Sandbox 處理過的話,請勿給 app 傳入 "vim" 參數,因為 Sandbox 會阻止之。
|
||||
/// - Parameters:
|
||||
/// - url: 檔案 URL。
|
||||
/// - app: 指定 App 應用的 binary 檔案名稱。
|
||||
static func openPhraseFile(fromURL url: URL, app: String = "") {
|
||||
if !Self.checkIfUserFilesExistBeforeOpening() { return }
|
||||
DispatchQueue.main.async {
|
||||
switch app {
|
||||
case "vim":
|
||||
let process = Process()
|
||||
let pipe = Pipe()
|
||||
process.executableURL = URL(fileURLWithPath: "/bin/sh/")
|
||||
process.arguments = ["-c", "open '/usr/bin/vim'", "'\(url.path)'"]
|
||||
process.standardOutput = pipe
|
||||
process.standardError = pipe
|
||||
process.terminationHandler = { process in
|
||||
vCLog("\ndidFinish: \(!process.isRunning)")
|
||||
}
|
||||
let fileHandle = pipe.fileHandleForReading
|
||||
do {
|
||||
try process.run()
|
||||
} catch {
|
||||
NSWorkspace.shared.openFile(url.path, withApplication: "TextEdit")
|
||||
}
|
||||
do {
|
||||
if let theData = try fileHandle.readToEnd(),
|
||||
let outStr = String(data: theData, encoding: .utf8)
|
||||
{
|
||||
vCLog(outStr)
|
||||
}
|
||||
} catch {}
|
||||
case "Finder":
|
||||
NSWorkspace.shared.activateFileViewerSelecting([url])
|
||||
default:
|
||||
if !NSWorkspace.shared.openFile(url.path, withApplication: app) {
|
||||
NSWorkspace.shared.openFile(url.path, withApplication: "TextEdit")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 檢查具體的使用者語彙檔案是否存在
|
||||
|
||||
private static func ensureFileExists(
|
||||
_ fileURL: URL, deployTemplate templateBasename: String = "1145141919810",
|
||||
extension ext: String = "txt"
|
||||
) -> Bool {
|
||||
let filePath = fileURL.path
|
||||
if !FileManager.default.fileExists(atPath: filePath) {
|
||||
let templateURL = Bundle.main.url(forResource: templateBasename, withExtension: ext)
|
||||
var templateData = Data("".utf8)
|
||||
if templateBasename != "" {
|
||||
do {
|
||||
try templateData = Data(contentsOf: templateURL ?? URL(fileURLWithPath: ""))
|
||||
} catch {
|
||||
templateData = Data("".utf8)
|
||||
}
|
||||
do {
|
||||
try templateData.write(to: URL(fileURLWithPath: filePath))
|
||||
} catch {
|
||||
vCLog("Failed to write template data to: \(filePath)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@discardableResult static func chkUserLMFilesExist(_ mode: Shared.InputMode) -> Bool {
|
||||
if !userDataFolderExists {
|
||||
return false
|
||||
}
|
||||
/// CandidateNode 資料與 UserOverrideModel 半衰模組資料檔案不需要強行確保存在。
|
||||
/// 前者的話,需要該檔案存在的人自己會建立。
|
||||
/// 後者的話,你在敲字時自己就會建立。
|
||||
var failed = false
|
||||
caseCheck: for type in vChewingLM.ReplacableUserDataType.allCases {
|
||||
let templateName = Self.templateName(for: type, mode: mode)
|
||||
if !ensureFileExists(userDictDataURL(mode: mode, type: type), deployTemplate: templateName) {
|
||||
failed = true
|
||||
break caseCheck
|
||||
}
|
||||
}
|
||||
failed = failed || !ensureFileExists(userSCPCSequencesURL(mode))
|
||||
return !failed
|
||||
}
|
||||
|
||||
internal static func templateName(for type: vChewingLM.ReplacableUserDataType, mode: Shared.InputMode) -> String {
|
||||
switch type {
|
||||
case .thePhrases: return kTemplateNameUserPhrases
|
||||
case .theFilter: return kTemplateNameUserFilterList
|
||||
case .theReplacements: return kTemplateNameUserReplacements
|
||||
case .theSymbols: return kTemplateNameUserSymbolPhrases
|
||||
case .theAssociates:
|
||||
return mode == .imeModeCHS ? kTemplateNameUserAssociatesCHS : kTemplateNameUserAssociatesCHT
|
||||
}
|
||||
}
|
||||
}
|
|
@ -33,22 +33,21 @@ extension SessionCtl: InputHandlerDelegate {
|
|||
|
||||
public func performUserPhraseOperation(addToFilter: Bool) -> Bool {
|
||||
guard let inputHandler = inputHandler, state.type == .ofMarking else { return false }
|
||||
if !LMMgr.writeUserPhrase(
|
||||
state.data.userPhraseDumped, inputMode: inputMode,
|
||||
areWeDeleting: addToFilter
|
||||
var succeeded = true
|
||||
|
||||
let kvPair = state.data.userPhraseKVPair
|
||||
var userPhrase = LMMgr.UserPhrase(
|
||||
keyArray: kvPair.keyArray, value: kvPair.value, inputMode: inputMode
|
||||
)
|
||||
|| !LMMgr.writeUserPhrase(
|
||||
state.data.userPhraseDumpedConverted, inputMode: inputMode.reversed,
|
||||
areWeDeleting: addToFilter
|
||||
)
|
||||
{
|
||||
return false
|
||||
if Self.areWeNerfing { userPhrase.weight = -114.514 }
|
||||
LMMgr.writeUserPhrasesAtOnce(userPhrase, areWeFiltering: addToFilter) {
|
||||
succeeded = false
|
||||
}
|
||||
if !succeeded { return false }
|
||||
|
||||
// 後續操作。
|
||||
let rawPair = state.data.userPhraseKVPair
|
||||
let valueCurrent = rawPair.1
|
||||
let valueReversed = ChineseConverter.crossConvert(rawPair.1)
|
||||
let valueCurrent = userPhrase.value
|
||||
let valueReversed = ChineseConverter.crossConvert(valueCurrent)
|
||||
|
||||
// 更新組字器內的單元圖資料。
|
||||
// 註:如果已經排除的內容是該讀音下唯一的記錄的話,
|
||||
|
@ -57,10 +56,10 @@ extension SessionCtl: InputHandlerDelegate {
|
|||
|
||||
// 因為上述操作不會立即生效(除非遞交組字區),所以暫時塞入臨時資料記錄。
|
||||
// 該臨時資料記錄會在接下來的語言模組資料重載過程中被自動清除。
|
||||
let temporaryScore: Double = SessionCtl.areWeNerfing ? -114.514 : 0
|
||||
LMMgr.currentLM.insertTemporaryData(
|
||||
keyArray: [rawPair.0], unigram: .init(value: rawPair.1, score: temporaryScore),
|
||||
isFiltering: SessionCtl.areWeNerfing
|
||||
keyArray: userPhrase.keyArray,
|
||||
unigram: .init(value: userPhrase.value, score: userPhrase.weight ?? 0),
|
||||
isFiltering: addToFilter
|
||||
)
|
||||
// 開始針對使用者半衰模組的清詞處理
|
||||
LMMgr.bleachSpecifiedSuggestions(targets: [valueCurrent], mode: IMEApp.currentInputMode)
|
||||
|
@ -179,32 +178,24 @@ extension SessionCtl: CtlCandidateDelegate {
|
|||
var succeeded = true
|
||||
|
||||
let rawPair = state.candidates[index]
|
||||
let theKey = rawPair.0.joined(separator: InputHandler.keySeparator)
|
||||
let valueCurrent = rawPair.1
|
||||
let valueReversed = ChineseConverter.crossConvert(rawPair.1)
|
||||
let nerfedScore = (action == .toNerf) ? " -114.514" : ""
|
||||
let convertedMark = "#𝙃𝙪𝙢𝙖𝙣𝘾𝙝𝙚𝙘𝙠𝙍𝙚𝙦𝙪𝙞𝙧𝙚𝙙"
|
||||
|
||||
let userPhraseDumped = "\(valueCurrent) \(theKey)\(nerfedScore)"
|
||||
let userPhraseDumpedConverted = "\(valueReversed) \(theKey)\(nerfedScore) \(convertedMark)"
|
||||
|
||||
if !LMMgr.writeUserPhrase(
|
||||
userPhraseDumped, inputMode: inputMode,
|
||||
areWeDeleting: action == .toFilter
|
||||
var userPhrase = LMMgr.UserPhrase(
|
||||
keyArray: rawPair.keyArray, value: rawPair.value, inputMode: inputMode
|
||||
)
|
||||
|| !LMMgr.writeUserPhrase(
|
||||
userPhraseDumpedConverted, inputMode: inputMode.reversed,
|
||||
areWeDeleting: action == .toFilter
|
||||
)
|
||||
{
|
||||
if action == .toNerf { userPhrase.weight = -114.514 }
|
||||
LMMgr.writeUserPhrasesAtOnce(userPhrase, areWeFiltering: action == .toFilter) {
|
||||
succeeded = false
|
||||
}
|
||||
|
||||
// 後續操作。
|
||||
let valueCurrent = userPhrase.value
|
||||
let valueReversed = ChineseConverter.crossConvert(valueCurrent)
|
||||
|
||||
// 因為上述操作不會立即生效(除非遞交組字區),所以暫時塞入臨時資料記錄。
|
||||
// 該臨時資料記錄會在接下來的語言模組資料重載過程中被自動清除。
|
||||
let temporaryScore: Double = (action == .toNerf) ? -114.514 : 0
|
||||
LMMgr.currentLM.insertTemporaryData(
|
||||
keyArray: rawPair.0, unigram: .init(value: rawPair.1, score: temporaryScore), isFiltering: action == .toFilter
|
||||
keyArray: userPhrase.keyArray,
|
||||
unigram: .init(value: userPhrase.value, score: userPhrase.weight ?? 0),
|
||||
isFiltering: action == .toFilter
|
||||
)
|
||||
|
||||
// 開始針對使用者半衰模組的清詞處理
|
||||
|
|
|
@ -214,7 +214,7 @@ extension CtlPrefWindow: NSTextViewDelegate, NSTextFieldDelegate {
|
|||
arrResult.append(weightVal.description)
|
||||
}
|
||||
if !txtPECommentField.stringValue.isEmpty { arrResult.append("#" + txtPECommentField.stringValue) }
|
||||
if LMMgr.checkIfUserPhraseExist(
|
||||
if LMMgr.shared.checkIfUserPhraseExist(
|
||||
userPhrase: txtPEField1.stringValue, mode: selInputMode, key: txtPEField2.stringValue
|
||||
) {
|
||||
arrResult.append(" #𝙾𝚟𝚎𝚛𝚛𝚒𝚍𝚎")
|
||||
|
|
|
@ -29,6 +29,9 @@
|
|||
5B2E009428FD1E8100E78D6E /* VwrPrefPaneCassette.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B2E009328FD1E8100E78D6E /* VwrPrefPaneCassette.swift */; };
|
||||
5B30BF282944867800BD87A9 /* CtlRevLookupWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B30BF272944867800BD87A9 /* CtlRevLookupWindow.swift */; };
|
||||
5B3133BF280B229700A4A505 /* InputHandler_HandleStates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B3133BE280B229700A4A505 /* InputHandler_HandleStates.swift */; };
|
||||
5B33844D29B8980100FCB497 /* LMMgr_UserPhraseStructure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B33844C29B8980100FCB497 /* LMMgr_UserPhraseStructure.swift */; };
|
||||
5B33844F29B8B4C200FCB497 /* LMMgr_PhraseEditorDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B33844E29B8B4C200FCB497 /* LMMgr_PhraseEditorDelegate.swift */; };
|
||||
5B33845129B8B61F00FCB497 /* LMMgr_Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B33845029B8B61F00FCB497 /* LMMgr_Utilities.swift */; };
|
||||
5B40113928D7050D00A9D4CB /* Shared in Frameworks */ = {isa = PBXBuildFile; productRef = 5B40113828D7050D00A9D4CB /* Shared */; };
|
||||
5B40113C28D71C0100A9D4CB /* Uninstaller in Frameworks */ = {isa = PBXBuildFile; productRef = 5B40113B28D71C0100A9D4CB /* Uninstaller */; };
|
||||
5B5A603028E81CC50001AE8D /* SwiftUIBackports in Frameworks */ = {isa = PBXBuildFile; productRef = 5B5A602F28E81CC50001AE8D /* SwiftUIBackports */; };
|
||||
|
@ -57,7 +60,7 @@
|
|||
5BA9FD1127FEDB6B002DE248 /* CtlPrefUIShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD0C27FEDB6B002DE248 /* CtlPrefUIShared.swift */; };
|
||||
5BA9FD1327FEDB6B002DE248 /* VwrPrefPaneDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD0E27FEDB6B002DE248 /* VwrPrefPaneDictionary.swift */; };
|
||||
5BAD0CD527D701F6003D127F /* vChewingKeyLayout.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 5B30F11227BA568800484E24 /* vChewingKeyLayout.bundle */; };
|
||||
5BAEFAD028012565001F42C9 /* LMMgr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BAEFACF28012565001F42C9 /* LMMgr.swift */; };
|
||||
5BAEFAD028012565001F42C9 /* LMMgr_Core.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BAEFACF28012565001F42C9 /* LMMgr_Core.swift */; };
|
||||
5BB1D7F42999027200EA8D2C /* PrefUITabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB1D7F32999027200EA8D2C /* PrefUITabs.swift */; };
|
||||
5BB802DA27FABA8300CF1C19 /* SessionCtl_Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB802D927FABA8300CF1C19 /* SessionCtl_Menu.swift */; };
|
||||
5BBBB75F27AED54C0023B93A /* Beep.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 5BBBB75D27AED54C0023B93A /* Beep.m4a */; };
|
||||
|
@ -224,6 +227,9 @@
|
|||
5B30F11227BA568800484E24 /* vChewingKeyLayout.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = vChewingKeyLayout.bundle; sourceTree = "<group>"; };
|
||||
5B312684287800DE001AA720 /* FAQ.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = FAQ.md; sourceTree = "<group>"; };
|
||||
5B3133BE280B229700A4A505 /* InputHandler_HandleStates.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = InputHandler_HandleStates.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
||||
5B33844C29B8980100FCB497 /* LMMgr_UserPhraseStructure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LMMgr_UserPhraseStructure.swift; sourceTree = "<group>"; };
|
||||
5B33844E29B8B4C200FCB497 /* LMMgr_PhraseEditorDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LMMgr_PhraseEditorDelegate.swift; sourceTree = "<group>"; };
|
||||
5B33845029B8B61F00FCB497 /* LMMgr_Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LMMgr_Utilities.swift; sourceTree = "<group>"; };
|
||||
5B40113A28D71B8700A9D4CB /* vChewing_Uninstaller */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = vChewing_Uninstaller; path = Packages/vChewing_Uninstaller; sourceTree = "<group>"; };
|
||||
5B5A602E28E81CB00001AE8D /* ShapsBenkau_SwiftUIBackports */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = ShapsBenkau_SwiftUIBackports; path = Packages/ShapsBenkau_SwiftUIBackports; sourceTree = "<group>"; };
|
||||
5B5C8ED628FC0E8E002C93A5 /* Sindresorhus_SSPreferences */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Sindresorhus_SSPreferences; path = Packages/Sindresorhus_SSPreferences; sourceTree = "<group>"; };
|
||||
|
@ -251,7 +257,7 @@
|
|||
5BA9FD0B27FEDB6B002DE248 /* VwrPrefPaneKeyboard.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = VwrPrefPaneKeyboard.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
||||
5BA9FD0C27FEDB6B002DE248 /* CtlPrefUIShared.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = CtlPrefUIShared.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
||||
5BA9FD0E27FEDB6B002DE248 /* VwrPrefPaneDictionary.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = VwrPrefPaneDictionary.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
||||
5BAEFACF28012565001F42C9 /* LMMgr.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = LMMgr.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
||||
5BAEFACF28012565001F42C9 /* LMMgr_Core.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = LMMgr_Core.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
||||
5BB1D7F32999027200EA8D2C /* PrefUITabs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefUITabs.swift; sourceTree = "<group>"; };
|
||||
5BB802D927FABA8300CF1C19 /* SessionCtl_Menu.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = SessionCtl_Menu.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
||||
5BBBB75D27AED54C0023B93A /* Beep.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = Beep.m4a; sourceTree = "<group>"; };
|
||||
|
@ -672,8 +678,6 @@
|
|||
6A0D4F1215FC0EB100ABF4B3 /* Modules */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5B62A33927AE7C6700A19448 /* UIModules */,
|
||||
5B62A33A27AE7C7500A19448 /* WindowControllers */,
|
||||
D427F76B278CA1BA004A2160 /* AppDelegate.swift */,
|
||||
5B8457A02871ADBE00C93B01 /* ChineseConverterBridge.swift */,
|
||||
5BF56F9728C39A2700DD6839 /* IMEState.swift */,
|
||||
|
@ -684,7 +688,10 @@
|
|||
5BE1F8A828F86AB5006C7FF5 /* InputHandler_HandleEvent.swift */,
|
||||
5B7F225C2808501000DDD3CB /* InputHandler_HandleInput.swift */,
|
||||
5B3133BE280B229700A4A505 /* InputHandler_HandleStates.swift */,
|
||||
5BAEFACF28012565001F42C9 /* LMMgr.swift */,
|
||||
5BAEFACF28012565001F42C9 /* LMMgr_Core.swift */,
|
||||
5B33844E29B8B4C200FCB497 /* LMMgr_PhraseEditorDelegate.swift */,
|
||||
5B33844C29B8980100FCB497 /* LMMgr_UserPhraseStructure.swift */,
|
||||
5B33845029B8B61F00FCB497 /* LMMgr_Utilities.swift */,
|
||||
D47B92BF27972AC800458394 /* main.swift */,
|
||||
5B963CA728D5DB1400DCEE88 /* PrefMgr_Core.swift */,
|
||||
5BCCAFF728DB19A300AB1B27 /* PrefMgr_Extension.swift */,
|
||||
|
@ -696,6 +703,8 @@
|
|||
5B00FA0B28DEC17200F6D436 /* SessionCtl_IMKCandidatesData.swift */,
|
||||
5BB802D927FABA8300CF1C19 /* SessionCtl_Menu.swift */,
|
||||
5B660A8528F64A8800E5E4F6 /* SymbolMenuDefaultData.swift */,
|
||||
5B62A33927AE7C6700A19448 /* UIModules */,
|
||||
5B62A33A27AE7C7500A19448 /* WindowControllers */,
|
||||
);
|
||||
path = Modules;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1096,7 +1105,9 @@
|
|||
D47B92C027972AD100458394 /* main.swift in Sources */,
|
||||
D4A13D5A27A59F0B003BE359 /* SessionCtl_Core.swift in Sources */,
|
||||
5B0EF55F28CDBF8E00F8F7CE /* CtlClientListMgr.swift in Sources */,
|
||||
5B33844D29B8980100FCB497 /* LMMgr_UserPhraseStructure.swift in Sources */,
|
||||
5B21177028753B9D000443A9 /* SessionCtl_Delegates.swift in Sources */,
|
||||
5B33845129B8B61F00FCB497 /* LMMgr_Utilities.swift in Sources */,
|
||||
5B21176E28753B35000443A9 /* SessionCtl_HandleDisplay.swift in Sources */,
|
||||
5BA9FD1027FEDB6B002DE248 /* VwrPrefPaneKeyboard.swift in Sources */,
|
||||
5B3133BF280B229700A4A505 /* InputHandler_HandleStates.swift in Sources */,
|
||||
|
@ -1107,10 +1118,11 @@
|
|||
5BD0113D2818543900609769 /* InputHandler_Core.swift in Sources */,
|
||||
5BF56F9A28C39D1800DD6839 /* IMEStateData.swift in Sources */,
|
||||
5BB1D7F42999027200EA8D2C /* PrefUITabs.swift in Sources */,
|
||||
5B33844F29B8B4C200FCB497 /* LMMgr_PhraseEditorDelegate.swift in Sources */,
|
||||
5B21176C287539BB000443A9 /* SessionCtl_HandleStates.swift in Sources */,
|
||||
5BE1F8A928F86AB5006C7FF5 /* InputHandler_HandleEvent.swift in Sources */,
|
||||
5B69938C293B811F0057CB8E /* VwrPrefPanePhrases.swift in Sources */,
|
||||
5BAEFAD028012565001F42C9 /* LMMgr.swift in Sources */,
|
||||
5BAEFAD028012565001F42C9 /* LMMgr_Core.swift in Sources */,
|
||||
5BCC631629407BBB00A2D84F /* CtlPrefWindow_PhraseEditor.swift in Sources */,
|
||||
5B782EC4280C243C007276DE /* InputHandler_HandleCandidate.swift in Sources */,
|
||||
5BA9FD0F27FEDB6B002DE248 /* VwrPrefPaneGeneral.swift in Sources */,
|
||||
|
|
Loading…
Reference in New Issue