vChewing-macOS/Source/Modules/LMMgr.swift

646 lines
24 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// (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 Shared
/// 使
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 enum 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 func currentLM() -> vChewingLM.LMInstantiator {
switch IMEApp.currentInputMode {
case .imeModeCHS:
return Self.lmCHS
case .imeModeCHT:
return Self.lmCHT
case .imeModeNULL:
return .init()
}
}
public static func 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
//
//
if PrefMgr.shared.associatedPhrasesEnabled { Self.loadUserAssociatesData() }
if PrefMgr.shared.phraseReplacementEnabled { Self.loadUserPhraseReplacement() }
if PrefMgr.shared.useSCPCTypingMode { Self.loadUserSCPCSequencesData() }
Self.loadUserPhrasesData()
}
public static func loadCoreLanguageModelFile(
filenameSansExtension: String, langModel lm: inout vChewingLM.LMInstantiator
) {
let dataPath: String = Self.getBundleDataPath(filenameSansExtension)
lm.loadLanguageModel(path: dataPath)
}
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(path: getBundleDataPath("data-cns"))
}
if !Self.lmCHT.isMiscDataLoaded {
Self.lmCHT.loadMiscData(path: getBundleDataPath("data-zhuyinwen"))
}
if !Self.lmCHT.isSymbolDataLoaded {
Self.lmCHT.loadSymbolData(path: getBundleDataPath("data-symbols"))
}
if !Self.lmCHS.isCNSDataLoaded {
Self.lmCHS.loadCNSData(path: getBundleDataPath("data-cns"))
}
if !Self.lmCHS.isMiscDataLoaded {
Self.lmCHS.loadMiscData(path: getBundleDataPath("data-zhuyinwen"))
}
if !Self.lmCHS.isSymbolDataLoaded {
Self.lmCHS.loadSymbolData(path: getBundleDataPath("data-symbols"))
}
group.leave()
}
if !Self.lmCHT.isLanguageModelLoaded {
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.isLanguageModelLoaded {
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(path: getBundleDataPath("data-cns"))
}
if !Self.lmCHS.isMiscDataLoaded {
Self.lmCHS.loadMiscData(path: getBundleDataPath("data-zhuyinwen"))
}
if !Self.lmCHS.isSymbolDataLoaded {
Self.lmCHS.loadSymbolData(path: getBundleDataPath("data-symbols"))
}
case .imeModeCHT:
if !Self.lmCHT.isCNSDataLoaded {
Self.lmCHT.loadCNSData(path: getBundleDataPath("data-cns"))
}
if !Self.lmCHT.isMiscDataLoaded {
Self.lmCHT.loadMiscData(path: getBundleDataPath("data-zhuyinwen"))
}
if !Self.lmCHT.isSymbolDataLoaded {
Self.lmCHT.loadSymbolData(path: getBundleDataPath("data-symbols"))
}
default: break
}
group.leave()
}
switch mode {
case .imeModeCHS:
if !Self.lmCHS.isLanguageModelLoaded {
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.isLanguageModelLoaded {
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 loadUserPhrasesData() {
Self.lmCHT.loadUserPhrasesData(
path: userPhrasesDataURL(.imeModeCHT).path,
filterPath: userFilteredDataURL(.imeModeCHT).path
)
Self.lmCHS.loadUserPhrasesData(
path: userPhrasesDataURL(.imeModeCHS).path,
filterPath: userFilteredDataURL(.imeModeCHS).path
)
Self.lmCHT.loadUserSymbolData(path: userSymbolDataURL(.imeModeCHT).path)
Self.lmCHS.loadUserSymbolData(path: userSymbolDataURL(.imeModeCHS).path)
Self.uomCHT.loadData(fromURL: userOverrideModelDataURL(.imeModeCHT))
Self.uomCHS.loadData(fromURL: userOverrideModelDataURL(.imeModeCHS))
CandidateNode.load(url: Self.userSymbolMenuDataURL())
}
public static func loadUserAssociatesData() {
Self.lmCHT.loadUserAssociatesData(
path: Self.userAssociatesDataURL(.imeModeCHT).path
)
Self.lmCHS.loadUserAssociatesData(
path: Self.userAssociatesDataURL(.imeModeCHS).path
)
}
public static func loadUserPhraseReplacement() {
Self.lmCHT.loadReplacementsData(
path: Self.userReplacementsDataURL(.imeModeCHT).path
)
Self.lmCHS.loadReplacementsData(
path: Self.userReplacementsDataURL(.imeModeCHS).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
) -> Bool {
switch mode {
case .imeModeCHS: return lmCHS.hasKeyValuePairFor(key: unigramKey, value: userPhrase)
case .imeModeCHT: return lmCHT.hasKeyValuePairFor(key: unigramKey, value: userPhrase)
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.lmCHS.isSCPCEnabled = state
Self.lmCHT.isSCPCEnabled = state
}
public static func setDeltaOfCalendarYears(_ delta: Int) {
Self.lmCHS.deltaOfCalendarYears = delta
Self.lmCHT.deltaOfCalendarYears = delta
}
// MARK: -
public static func getBundleDataPath(_ filenameSansExt: String) -> String {
Bundle.main.path(forResource: filenameSansExt, ofType: "plist")!
}
// MARK: - 使
// Swift appendingPathComponent URL .path
/// 使
/// - Parameter mode:
/// - Returns: URL
public static func userPhrasesDataURL(_ mode: Shared.InputMode) -> URL {
let fileName = (mode == .imeModeCHT) ? "userdata-cht.txt" : "userdata-chs.txt"
return URL(fileURLWithPath: dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName)
}
/// 使
/// - Parameter mode:
/// - Returns: URL
public static func userSymbolDataURL(_ mode: Shared.InputMode) -> URL {
let fileName = (mode == .imeModeCHT) ? "usersymbolphrases-cht.txt" : "usersymbolphrases-chs.txt"
return URL(fileURLWithPath: dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName)
}
/// 使
/// - Parameter mode:
/// - Returns: URL
public static func userAssociatesDataURL(_ mode: Shared.InputMode) -> URL {
let fileName = (mode == .imeModeCHT) ? "associatedPhrases-cht.txt" : "associatedPhrases-chs.txt"
return URL(fileURLWithPath: dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName)
}
/// 使
/// - Parameter mode:
/// - Returns: URL
public static func userFilteredDataURL(_ mode: Shared.InputMode) -> URL {
let fileName = (mode == .imeModeCHT) ? "exclude-phrases-cht.txt" : "exclude-phrases-chs.txt"
return URL(fileURLWithPath: dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName)
}
/// 使
/// - Parameter mode:
/// - Returns: URL
public static func userReplacementsDataURL(_ mode: Shared.InputMode) -> URL {
let fileName = (mode == .imeModeCHT) ? "phrases-replacement-cht.txt" : "phrases-replacement-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
///
///
if !ensureFileExists(userPhrasesDataURL(mode), deployTemplate: kTemplateNameUserPhrases)
|| !ensureFileExists(
userAssociatesDataURL(mode),
deployTemplate: mode == .imeModeCHS ? kTemplateNameUserAssociatesCHS : kTemplateNameUserAssociatesCHT
)
|| !ensureFileExists(userSCPCSequencesURL(mode))
|| !ensureFileExists(userFilteredDataURL(mode), deployTemplate: kTemplateNameUserFilterList)
|| !ensureFileExists(userReplacementsDataURL(mode), deployTemplate: kTemplateNameUserReplacements)
|| !ensureFileExists(userSymbolDataURL(mode), deployTemplate: kTemplateNameUserSymbolPhrases)
{
return false
}
return true
}
// 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 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
}
// MARK: - 使
public static func resetSpecifiedUserDataFolder() {
UserDefaults.standard.removeObject(forKey: UserDef.kUserDataFolderSpecified.rawValue)
LMMgr.initUserLangModels()
}
// MARK: - 使
public static func writeUserPhrase(
_ userPhrase: String?, inputMode mode: Shared.InputMode, areWeDuplicating: Bool, areWeDeleting: Bool
) -> Bool {
if var currentMarkedPhrase: String = userPhrase {
if !chkUserLMFilesExist(.imeModeCHS)
|| !chkUserLMFilesExist(.imeModeCHT)
{
return false
}
let theURL = areWeDeleting ? userFilteredDataURL(mode) : userPhrasesDataURL(mode)
if areWeDuplicating, !areWeDeleting {
// Do not use ASCII characters to comment here.
// Otherwise, it will be scrambled by cnvHYPYtoBPMF
// module shipped in the vChewing Phrase Editor.
currentMarkedPhrase += "\t#𝙾𝚟𝚎𝚛𝚛𝚒𝚍𝚎"
}
if let writeFile = FileHandle(forUpdatingAtPath: theURL.path),
let data = currentMarkedPhrase.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 !PrefMgr.shared.shouldAutoReloadUserDataFiles {}
loadUserPhrasesData()
return true
}
return false
}
// 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.setActivationPolicy(.accessory)
}
return false
}
return true
}
public static func openPhraseFile(fromURL url: URL) {
if !Self.checkIfUserFilesExistBeforeOpening() { return }
DispatchQueue.main.async {
NSWorkspace.shared.openFile(url.path, withApplication: "vChewingPhraseEditor")
}
}
// 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
}
}
}