Repo // Refactoring the entire LMMgr.

This commit is contained in:
ShikiSuen 2023-03-08 17:45:46 +08:00
parent 03b4fb683d
commit e6696d910d
11 changed files with 1032 additions and 974 deletions

View File

@ -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()
}

View File

@ -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 }
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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
)
// 使

View File

@ -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(" #𝙾𝚟𝚎𝚛𝚛𝚒𝚍𝚎")

View File

@ -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 */,