From 4b87138dc08f1a08c3f6a614809de799ed062c7d Mon Sep 17 00:00:00 2001 From: ShikiSuen Date: Sat, 9 Apr 2022 00:30:11 +0800 Subject: [PATCH] mgrLM // Swiftify everything except Cpp-related stuff. - This is not a complete swiftification since all ObjCpp-related parts are not swiftifiable. - Rename invalid folder path target in lieu of removal. - Let dataFolderPath() always ensure trailing slash. - Also added process of verifying folder write access. - Also simplify chkUserLMFilesExist(InputMode). - Also enveloped LMConsolidator into an ObjC command in order to swiftify and refactor writeUserPhrase(). --- Source/Modules/IMEModules/IME.swift | 4 +- .../Modules/LangModelRelated/mgrLangModel.h | 16 +- .../Modules/LangModelRelated/mgrLangModel.mm | 278 ++---------------- .../LangModelRelated/mgrLangModel.swift | 242 +++++++++++++++ vChewing.xcodeproj/project.pbxproj | 4 + 5 files changed, 280 insertions(+), 264 deletions(-) create mode 100644 Source/Modules/LangModelRelated/mgrLangModel.swift diff --git a/Source/Modules/IMEModules/IME.swift b/Source/Modules/IMEModules/IME.swift index 3aa8290a..2616ca25 100644 --- a/Source/Modules/IMEModules/IME.swift +++ b/Source/Modules/IMEModules/IME.swift @@ -85,7 +85,9 @@ import Cocoa // MARK: - Open a phrase data file. static func openPhraseFile(userFileAt path: String) { func checkIfUserFilesExist() -> Bool { - if !mgrLangModel.checkIfUserLanguageModelFilesExist() { + if !mgrLangModel.chkUserLMFilesExist(InputMode.imeModeCHS) + || !mgrLangModel.chkUserLMFilesExist(InputMode.imeModeCHT) + { let content = String( format: NSLocalizedString( "Please check the permission at \"%@\".", comment: ""), diff --git a/Source/Modules/LangModelRelated/mgrLangModel.h b/Source/Modules/LangModelRelated/mgrLangModel.h index 816d44cb..b7bfbae1 100644 --- a/Source/Modules/LangModelRelated/mgrLangModel.h +++ b/Source/Modules/LangModelRelated/mgrLangModel.h @@ -35,29 +35,15 @@ NS_ASSUME_NONNULL_BEGIN + (void)loadUserPhrases; + (void)loadUserAssociatedPhrases; + (void)loadUserPhraseReplacement; -+ (BOOL)checkIfUserLanguageModelFilesExist; -+ (BOOL)checkIfUserDataFolderExists; -+ (BOOL)checkIfSpecifiedUserDataFolderValid:(NSString *)folderPath; -+ (NSString *)dataFolderPath:(bool)isDefaultFolder NS_SWIFT_NAME(dataFolderPath(isDefaultFolder:)); + (BOOL)checkIfUserPhraseExist:(NSString *)userPhrase inputMode:(InputMode)mode key:(NSString *)key NS_SWIFT_NAME(checkIfUserPhraseExist(userPhrase:mode:key:)); -+ (BOOL)writeUserPhrase:(NSString *)userPhrase - inputMode:(InputMode)mode - areWeDuplicating:(BOOL)areWeDuplicating - areWeDeleting:(BOOL)areWeDeleting; ++ (void)consolidateGivenFile:(NSString *)path shouldCheckPragma:(BOOL)shouldCheckPragma; + (void)setPhraseReplacementEnabled:(BOOL)phraseReplacementEnabled; + (void)setCNSEnabled:(BOOL)cnsEnabled; + (void)setSymbolEnabled:(BOOL)symbolEnabled; -+ (NSString *)specifyBundleDataPath:(NSString *)filename; -+ (NSString *)userPhrasesDataPath:(InputMode)mode; -+ (NSString *)userSymbolDataPath:(InputMode)mode; -+ (NSString *)userAssociatedPhrasesDataPath:(InputMode)mode; -+ (NSString *)excludedPhrasesDataPath:(InputMode)mode; -+ (NSString *)phraseReplacementDataPath:(InputMode)mode; - @end /// The following methods are merely for testing. diff --git a/Source/Modules/LangModelRelated/mgrLangModel.mm b/Source/Modules/LangModelRelated/mgrLangModel.mm index 9b055161..081c798d 100644 --- a/Source/Modules/LangModelRelated/mgrLangModel.mm +++ b/Source/Modules/LangModelRelated/mgrLangModel.mm @@ -37,28 +37,16 @@ static vChewing::LMInstantiator gLangModelCHS; static vChewing::UserOverrideModel gUserOverrideModelCHT(kUserOverrideModelCapacity, kObservedOverrideHalflife); static vChewing::UserOverrideModel gUserOverrideModelCHS(kUserOverrideModelCapacity, kObservedOverrideHalflife); -static NSString *const kUserDataTemplateName = @"template-data"; -static NSString *const kUserAssDataTemplateName = @"template-data"; -static NSString *const kExcludedPhrasesvChewingTemplateName = @"template-exclude-phrases"; -static NSString *const kPhraseReplacementTemplateName = @"template-phrases-replacement"; -static NSString *const kUserSymbolDataTemplateName = @"template-user-symbol-data"; -static NSString *const kTemplateExtension = @".txt"; - @implementation mgrLangModel +// 這個函數無法遷移至 Swift static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, vChewing::LMInstantiator &lm) { - Class cls = NSClassFromString(@"ctlInputMethod"); - NSString *dataPath = [[NSBundle bundleForClass:cls] pathForResource:filenameWithoutExtension ofType:@"txt"]; + NSString *dataPath = [mgrLangModel getBundleDataPath:filenameWithoutExtension]; lm.loadLanguageModel([dataPath UTF8String]); } -+ (NSString *)specifyBundleDataPath:(NSString *)filenameWithoutExtension; -{ - Class cls = NSClassFromString(@"ctlInputMethod"); - return [[NSBundle bundleForClass:cls] pathForResource:filenameWithoutExtension ofType:@"txt"]; -} - +// 這個函數無法遷移至 Swift + (void)loadDataModels { if (!gLangModelCHT.isDataModelLoaded()) @@ -67,15 +55,15 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, vChewing } if (!gLangModelCHT.isMiscDataLoaded()) { - gLangModelCHT.loadMiscData([[self specifyBundleDataPath:@"data-zhuyinwen"] UTF8String]); + gLangModelCHT.loadMiscData([[self getBundleDataPath:@"data-zhuyinwen"] UTF8String]); } if (!gLangModelCHT.isSymbolDataLoaded()) { - gLangModelCHT.loadSymbolData([[self specifyBundleDataPath:@"data-symbols"] UTF8String]); + gLangModelCHT.loadSymbolData([[self getBundleDataPath:@"data-symbols"] UTF8String]); } if (!gLangModelCHT.isCNSDataLoaded()) { - gLangModelCHT.loadCNSData([[self specifyBundleDataPath:@"char-kanji-cns"] UTF8String]); + gLangModelCHT.loadCNSData([[self getBundleDataPath:@"char-kanji-cns"] UTF8String]); } // ----------------- if (!gLangModelCHS.isDataModelLoaded()) @@ -84,18 +72,19 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, vChewing } if (!gLangModelCHS.isMiscDataLoaded()) { - gLangModelCHS.loadMiscData([[self specifyBundleDataPath:@"data-zhuyinwen"] UTF8String]); + gLangModelCHS.loadMiscData([[self getBundleDataPath:@"data-zhuyinwen"] UTF8String]); } if (!gLangModelCHS.isSymbolDataLoaded()) { - gLangModelCHS.loadSymbolData([[self specifyBundleDataPath:@"data-symbols"] UTF8String]); + gLangModelCHS.loadSymbolData([[self getBundleDataPath:@"data-symbols"] UTF8String]); } if (!gLangModelCHS.isCNSDataLoaded()) { - gLangModelCHS.loadCNSData([[self specifyBundleDataPath:@"char-kanji-cns"] UTF8String]); + gLangModelCHS.loadCNSData([[self getBundleDataPath:@"char-kanji-cns"] UTF8String]); } } +// 這個函數無法遷移至 Swift + (void)loadDataModel:(InputMode)mode { if ([mode isEqualToString:imeModeCHT]) @@ -106,15 +95,15 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, vChewing } if (!gLangModelCHT.isMiscDataLoaded()) { - gLangModelCHT.loadMiscData([[self specifyBundleDataPath:@"data-zhuyinwen"] UTF8String]); + gLangModelCHT.loadMiscData([[self getBundleDataPath:@"data-zhuyinwen"] UTF8String]); } if (!gLangModelCHT.isSymbolDataLoaded()) { - gLangModelCHT.loadSymbolData([[self specifyBundleDataPath:@"data-symbols"] UTF8String]); + gLangModelCHT.loadSymbolData([[self getBundleDataPath:@"data-symbols"] UTF8String]); } if (!gLangModelCHT.isCNSDataLoaded()) { - gLangModelCHT.loadCNSData([[self specifyBundleDataPath:@"char-kanji-cns"] UTF8String]); + gLangModelCHT.loadCNSData([[self getBundleDataPath:@"char-kanji-cns"] UTF8String]); } } @@ -126,19 +115,20 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, vChewing } if (!gLangModelCHS.isMiscDataLoaded()) { - gLangModelCHS.loadMiscData([[self specifyBundleDataPath:@"data-zhuyinwen"] UTF8String]); + gLangModelCHS.loadMiscData([[self getBundleDataPath:@"data-zhuyinwen"] UTF8String]); } if (!gLangModelCHS.isSymbolDataLoaded()) { - gLangModelCHS.loadSymbolData([[self specifyBundleDataPath:@"data-symbols"] UTF8String]); + gLangModelCHS.loadSymbolData([[self getBundleDataPath:@"data-symbols"] UTF8String]); } if (!gLangModelCHS.isCNSDataLoaded()) { - gLangModelCHS.loadCNSData([[self specifyBundleDataPath:@"char-kanji-cns"] UTF8String]); + gLangModelCHS.loadCNSData([[self getBundleDataPath:@"char-kanji-cns"] UTF8String]); } } } +// 這個函數無法遷移至 Swift + (void)loadUserPhrases { gLangModelCHT.loadUserPhrases([[self userPhrasesDataPath:imeModeCHT] UTF8String], @@ -149,136 +139,21 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, vChewing gLangModelCHS.loadUserSymbolData([[self userSymbolDataPath:imeModeCHS] UTF8String]); } +// 這個函數無法遷移至 Swift + (void)loadUserAssociatedPhrases { gLangModelCHT.loadUserAssociatedPhrases([[self userAssociatedPhrasesDataPath:imeModeCHT] UTF8String]); gLangModelCHS.loadUserAssociatedPhrases([[self userAssociatedPhrasesDataPath:imeModeCHS] UTF8String]); } +// 這個函數無法遷移至 Swift + (void)loadUserPhraseReplacement { gLangModelCHT.loadPhraseReplacementMap([[self phraseReplacementDataPath:imeModeCHT] UTF8String]); gLangModelCHS.loadPhraseReplacementMap([[self phraseReplacementDataPath:imeModeCHS] UTF8String]); } -+ (BOOL)checkIfUserDataFolderExists -{ - NSString *folderPath = [self dataFolderPath:false]; - BOOL isFolder = NO; - BOOL folderExist = [[NSFileManager defaultManager] fileExistsAtPath:folderPath isDirectory:&isFolder]; - if (folderExist && !isFolder) - { - NSError *error = nil; - [[NSFileManager defaultManager] removeItemAtPath:folderPath error:&error]; - if (error) - { - NSLog(@"Failed to remove folder %@", error); - return NO; - } - folderExist = NO; - } - if (!folderExist) - { - NSError *error = nil; - [[NSFileManager defaultManager] createDirectoryAtPath:folderPath - withIntermediateDirectories:YES - attributes:nil - error:&error]; - if (error) - { - NSLog(@"Failed to create folder %@", error); - return NO; - } - } - return YES; -} - -+ (BOOL)checkIfSpecifiedUserDataFolderValid:(NSString *)folderPath -{ - BOOL isFolder = NO; - BOOL folderExist = [[NSFileManager defaultManager] fileExistsAtPath:folderPath isDirectory:&isFolder]; - if ((folderExist && !isFolder) || (!folderExist)) - { - return NO; - } - return YES; -} - -+ (BOOL)ensureFileExists:(NSString *)filePath - populateWithTemplate:(NSString *)templateBasename - extension:(NSString *)ext -{ - if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) - { - - NSURL *templateURL = [[NSBundle mainBundle] URLForResource:templateBasename withExtension:ext]; - NSData *templateData; - if (templateURL) - { - templateData = [NSData dataWithContentsOfURL:templateURL]; - } - else - { - templateData = [@"" dataUsingEncoding:NSUTF8StringEncoding]; - } - - BOOL result = [templateData writeToFile:filePath atomically:YES]; - if (!result) - { - NSLog(@"Failed to write file"); - return NO; - } - } - return YES; -} - -+ (BOOL)checkIfUserLanguageModelFilesExist -{ - if (![self checkIfUserDataFolderExists]) - return NO; - if (![self ensureFileExists:[self userPhrasesDataPath:imeModeCHS] - populateWithTemplate:kUserDataTemplateName - extension:kTemplateExtension]) - return NO; - if (![self ensureFileExists:[self userPhrasesDataPath:imeModeCHT] - populateWithTemplate:kUserDataTemplateName - extension:kTemplateExtension]) - return NO; - if (![self ensureFileExists:[self userAssociatedPhrasesDataPath:imeModeCHS] - populateWithTemplate:kUserAssDataTemplateName - extension:kTemplateExtension]) - return NO; - if (![self ensureFileExists:[self userAssociatedPhrasesDataPath:imeModeCHT] - populateWithTemplate:kUserAssDataTemplateName - extension:kTemplateExtension]) - return NO; - if (![self ensureFileExists:[self excludedPhrasesDataPath:imeModeCHS] - populateWithTemplate:kExcludedPhrasesvChewingTemplateName - extension:kTemplateExtension]) - return NO; - if (![self ensureFileExists:[self excludedPhrasesDataPath:imeModeCHT] - populateWithTemplate:kExcludedPhrasesvChewingTemplateName - extension:kTemplateExtension]) - return NO; - if (![self ensureFileExists:[self phraseReplacementDataPath:imeModeCHS] - populateWithTemplate:kPhraseReplacementTemplateName - extension:kTemplateExtension]) - return NO; - if (![self ensureFileExists:[self phraseReplacementDataPath:imeModeCHT] - populateWithTemplate:kPhraseReplacementTemplateName - extension:kTemplateExtension]) - return NO; - if (![self ensureFileExists:[self userSymbolDataPath:imeModeCHT] - populateWithTemplate:kUserSymbolDataTemplateName - extension:kTemplateExtension]) - return NO; - if (![self ensureFileExists:[self userSymbolDataPath:imeModeCHS] - populateWithTemplate:kUserSymbolDataTemplateName - extension:kTemplateExtension]) - return NO; - return YES; -} - +// 這個函數無法遷移至 Swift + (BOOL)checkIfUserPhraseExist:(NSString *)userPhrase inputMode:(InputMode)mode key:(NSString *)key NS_SWIFT_NAME(checkIfUserPhraseExist(userPhrase:mode:key:)) @@ -297,144 +172,51 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, vChewing return NO; } -+ (BOOL)writeUserPhrase:(NSString *)userPhrase - inputMode:(InputMode)mode - areWeDuplicating:(BOOL)areWeDuplicating - areWeDeleting:(BOOL)areWeDeleting +// 這個函數無法遷移至 Swift ++ (void)consolidateGivenFile:(NSString *)path shouldCheckPragma:(BOOL)shouldCheckPragma { - if (![self checkIfUserLanguageModelFilesExist]) - { - return NO; - } - - // BOOL addLineBreakAtFront = NO; - NSString *path = areWeDeleting ? [self excludedPhrasesDataPath:mode] : [self userPhrasesDataPath:mode]; - - NSMutableString *currentMarkedPhrase = [NSMutableString string]; - // if (addLineBreakAtFront) { - // [currentMarkedPhrase appendString:@"\n"]; - // } - [currentMarkedPhrase appendString:userPhrase]; - 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 appendString:@"\t#𝙾𝚟𝚎𝚛𝚛𝚒𝚍𝚎"]; - } - [currentMarkedPhrase appendString:@"\n"]; - - NSFileHandle *writeFile = [NSFileHandle fileHandleForUpdatingAtPath:path]; - if (!writeFile) - { - return NO; - } - [writeFile seekToEndOfFile]; - NSData *data = [currentMarkedPhrase dataUsingEncoding:NSUTF8StringEncoding]; - [writeFile writeData:data]; - [writeFile closeFile]; - - // We enforce the format consolidation here, since the pragma header will let the UserPhraseLM bypasses the - // consolidating process on load. - vChewing::LMConsolidator::ConsolidateContent([path UTF8String], false); - - // We use FSEventStream to monitor the change of the user phrase folder, - // so we don't have to load data here unless FSEventStream is disabled by user. - if (!mgrPrefs.shouldAutoReloadUserDataFiles) - { - [self loadUserPhrases]; - } - return YES; -} - -+ (NSString *)dataFolderPath:(bool)isDefaultFolder -{ - // 此處不能用「~」來取代當前使用者目錄名稱。不然的話,一旦輸入法被系統的沙箱干預的話,則反而會定位到沙箱目錄內。 - NSString *appSupportPath = [NSFileManager.defaultManager URLsForDirectory:NSApplicationSupportDirectory - inDomains:NSUserDomainMask][0].path; - NSString *userDictPath = [appSupportPath stringByAppendingPathComponent:@"vChewing"].stringByExpandingTildeInPath; - if (mgrPrefs.userDataFolderSpecified.stringByExpandingTildeInPath == userDictPath || isDefaultFolder) - { - return userDictPath; - } - if ([mgrPrefs ifSpecifiedUserDataPathExistsInPlist]) - { - if ([self checkIfSpecifiedUserDataFolderValid:mgrPrefs.userDataFolderSpecified.stringByExpandingTildeInPath]) - { - return mgrPrefs.userDataFolderSpecified.stringByExpandingTildeInPath; - } - else - { - [NSUserDefaults.standardUserDefaults removeObjectForKey:@"UserDataFolderSpecified"]; - } - } - return userDictPath; -} - -+ (NSString *)userPhrasesDataPath:(InputMode)mode; -{ - NSString *fileName = [mode isEqualToString:imeModeCHT] ? @"userdata-cht.txt" : @"userdata-chs.txt"; - return [[self dataFolderPath:false] stringByAppendingPathComponent:fileName]; -} - -+ (NSString *)userSymbolDataPath:(InputMode)mode; -{ - NSString *fileName = - [mode isEqualToString:imeModeCHT] ? @"usersymbolphrases-cht.txt" : @"usersymbolphrases-chs.txt"; - return [[self dataFolderPath:false] stringByAppendingPathComponent:fileName]; -} - -+ (NSString *)userAssociatedPhrasesDataPath:(InputMode)mode; -{ - NSString *fileName = - [mode isEqualToString:imeModeCHT] ? @"associatedPhrases-cht.txt" : @"associatedPhrases-chs.txt"; - return [[self dataFolderPath:false] stringByAppendingPathComponent:fileName]; -} - -+ (NSString *)excludedPhrasesDataPath:(InputMode)mode; -{ - NSString *fileName = [mode isEqualToString:imeModeCHT] ? @"exclude-phrases-cht.txt" : @"exclude-phrases-chs.txt"; - return [[self dataFolderPath:false] stringByAppendingPathComponent:fileName]; -} - -+ (NSString *)phraseReplacementDataPath:(InputMode)mode; -{ - NSString *fileName = - [mode isEqualToString:imeModeCHT] ? @"phrases-replacement-cht.txt" : @"phrases-replacement-chs.txt"; - return [[self dataFolderPath:false] stringByAppendingPathComponent:fileName]; + vChewing::LMConsolidator::ConsolidateContent([path UTF8String], shouldCheckPragma); } +// 這個函數無法遷移至 Swift + (vChewing::LMInstantiator *)lmCHT { return &gLangModelCHT; } +// 這個函數無法遷移至 Swift + (vChewing::LMInstantiator *)lmCHS { return &gLangModelCHS; } +// 這個函數無法遷移至 Swift + (vChewing::UserOverrideModel *)userOverrideModelCHT { return &gUserOverrideModelCHT; } +// 這個函數無法遷移至 Swift + (vChewing::UserOverrideModel *)userOverrideModelCHS { return &gUserOverrideModelCHS; } +// 這個函數無法遷移至 Swift + (void)setPhraseReplacementEnabled:(BOOL)phraseReplacementEnabled { gLangModelCHT.setPhraseReplacementEnabled(phraseReplacementEnabled); gLangModelCHS.setPhraseReplacementEnabled(phraseReplacementEnabled); } +// 這個函數無法遷移至 Swift + (void)setCNSEnabled:(BOOL)cnsEnabled { gLangModelCHT.setCNSEnabled(cnsEnabled); gLangModelCHS.setCNSEnabled(cnsEnabled); } +// 這個函數無法遷移至 Swift + (void)setSymbolEnabled:(BOOL)symbolEnabled { gLangModelCHT.setSymbolEnabled(symbolEnabled); diff --git a/Source/Modules/LangModelRelated/mgrLangModel.swift b/Source/Modules/LangModelRelated/mgrLangModel.swift new file mode 100644 index 00000000..9b99890a --- /dev/null +++ b/Source/Modules/LangModelRelated/mgrLangModel.swift @@ -0,0 +1,242 @@ +// Copyright (c) 2021 and onwards The vChewing Project (MIT-NTL License). +/* +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +1. The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +2. No trademark license is granted to use the trade names, trademarks, service +marks, or product names of Contributor, except as required to fulfill notice +requirements above. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +import Cocoa + +@objc extension mgrLangModel { + + // MARK: - 獲取當前輸入法封包內的原廠核心語彙檔案所在路徑 + static func getBundleDataPath(_ filenameSansExt: String) -> String { + return Bundle.main.path(forResource: filenameSansExt, ofType: "txt")! + } + + // MARK: - 使用者語彙檔案的具體檔案名稱路徑定義 + // Swift 的 appendingPathComponent 需要藉由 URL 完成,最後再用 .path 轉為路徑。 + + static func userPhrasesDataPath(_ mode: InputMode) -> String { + let fileName = (mode == InputMode.imeModeCHT) ? "userdata-cht.txt" : "userdata-chs.txt" + return URL(fileURLWithPath: self.dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName).path + } + + static func userSymbolDataPath(_ mode: InputMode) -> String { + let fileName = (mode == InputMode.imeModeCHT) ? "usersymbolphrases-cht.txt" : "usersymbolphrases-chs.txt" + return URL(fileURLWithPath: self.dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName).path + } + + static func userAssociatedPhrasesDataPath(_ mode: InputMode) -> String { + let fileName = (mode == InputMode.imeModeCHT) ? "associatedPhrases-cht.txt" : "associatedPhrases-chs.txt" + return URL(fileURLWithPath: self.dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName).path + } + + static func excludedPhrasesDataPath(_ mode: InputMode) -> String { + let fileName = (mode == InputMode.imeModeCHT) ? "exclude-phrases-cht.txt" : "exclude-phrases-chs.txt" + return URL(fileURLWithPath: self.dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName).path + } + + static func phraseReplacementDataPath(_ mode: InputMode) -> String { + let fileName = (mode == InputMode.imeModeCHT) ? "phrases-replacement-cht.txt" : "phrases-replacement-chs.txt" + return URL(fileURLWithPath: self.dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName).path + } + + // MARK: - 檢查具體的使用者語彙檔案是否存在 + + static func ensureFileExists( + _ filePath: String, populateWithTemplate templateBasename: String = "1145141919810", + extension ext: String = "txt" + ) -> Bool { + 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 { + IME.prtDebugIntel("Failed to write file") + return false + } + } + } + return true + } + + static func chkUserLMFilesExist(_ mode: InputMode) -> Bool { + if !self.checkIfUserDataFolderExists() { + return false + } + if !ensureFileExists(userPhrasesDataPath(mode)) + || !ensureFileExists(userAssociatedPhrasesDataPath(mode)) + || !ensureFileExists(excludedPhrasesDataPath(mode)) + || !ensureFileExists(phraseReplacementDataPath(mode)) + || !ensureFileExists(userSymbolDataPath(mode)) + { + return false + } + + return true + } + + // 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 ?? "") + + if ((folderExist && !isFolder.boolValue) || !folderExist) || !isFolderWritable { + return false + } + + return true + } + + // ⚠︎ 私有函數:檢查且糾偏,不接受任何傳入變數。該函數不用於其他型別。 + // 待辦事項:擇日合併至另一個同類型的函數當中。 + static func checkIfUserDataFolderExists() -> Bool { + let folderPath = mgrLangModel.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 self.dataFolderPath(isDefaultFolder: false) + == self.dataFolderPath(isDefaultFolder: true) + { + let formatter = DateFormatter.init() + 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: - 用以讀取使用者語彙檔案目錄的函數,會自動對 mgrPrefs 當中的參數糾偏。 + // 當且僅當 mgrPrefs 當中的參數不合規(比如非實在路徑、或者無權限寫入)時,才會糾偏。 + + static func dataFolderPath(isDefaultFolder: Bool) -> String { + let appSupportPath = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0].path + var userDictPathSpecified = (mgrPrefs.userDataFolderSpecified as NSString).expandingTildeInPath + var userDictPathDefault = + (URL(fileURLWithPath: appSupportPath).appendingPathComponent("vChewing").path as NSString) + .expandingTildeInPath + + userDictPathDefault.ensureTrailingSlash() + userDictPathSpecified.ensureTrailingSlash() + + if (userDictPathSpecified == userDictPathDefault) + || isDefaultFolder + { + return userDictPathDefault + } + if mgrPrefs.ifSpecifiedUserDataPathExistsInPlist() { + if mgrLangModel.checkIfSpecifiedUserDataFolderValid(userDictPathSpecified) { + return userDictPathSpecified + } else { + UserDefaults.standard.removeObject(forKey: "UserDataFolderSpecified") + } + } + return userDictPathDefault + } + + // MARK: - 寫入使用者檔案 + static func writeUserPhrase( + _ userPhrase: String?, inputMode mode: InputMode, areWeDuplicating: Bool, areWeDeleting: Bool + ) -> Bool { + if var currentMarkedPhrase: String = userPhrase { + if !self.chkUserLMFilesExist(InputMode.imeModeCHS) + || !self.chkUserLMFilesExist(InputMode.imeModeCHT) + { + return false + } + + let path = areWeDeleting ? self.excludedPhrasesDataPath(mode) : self.userPhrasesDataPath(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#𝙾𝚟𝚎𝚛𝚛𝚒𝚍𝚎" + } + currentMarkedPhrase += "\n" + + if let writeFile = FileHandle(forUpdatingAtPath: path), + let data = currentMarkedPhrase.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. + self.consolidate(givenFile: path, shouldCheckPragma: false) + + // We use FSEventStream to monitor possible changes of the user phrase folder, hence the + // lack of the needs of manually load data here unless FSEventStream is disabled by user. + if !mgrPrefs.shouldAutoReloadUserDataFiles { + self.loadUserPhrases() + } + return true + } + return false + } + +} diff --git a/vChewing.xcodeproj/project.pbxproj b/vChewing.xcodeproj/project.pbxproj index 43f06194..1c2e109f 100644 --- a/vChewing.xcodeproj/project.pbxproj +++ b/vChewing.xcodeproj/project.pbxproj @@ -50,6 +50,7 @@ 5BA9FD4A27FEF3C9002DE248 /* PreferencesTabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD3D27FEF3C8002DE248 /* PreferencesTabViewController.swift */; }; 5BA9FD8B28006B41002DE248 /* VDKComboBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD8A28006B41002DE248 /* VDKComboBox.swift */; }; 5BAD0CD527D701F6003D127F /* vChewingKeyLayout.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 5B30F11227BA568800484E24 /* vChewingKeyLayout.bundle */; }; + 5BAEFAD028012565001F42C9 /* mgrLangModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BAEFACF28012565001F42C9 /* mgrLangModel.swift */; }; 5BB802DA27FABA8300CF1C19 /* ctlInputMethod_Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB802D927FABA8300CF1C19 /* ctlInputMethod_Menu.swift */; }; 5BBBB75F27AED54C0023B93A /* Beep.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 5BBBB75D27AED54C0023B93A /* Beep.m4a */; }; 5BBBB76027AED54C0023B93A /* Fart.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 5BBBB75E27AED54C0023B93A /* Fart.m4a */; }; @@ -227,6 +228,7 @@ 5BA9FD3C27FEF3C8002DE248 /* Section.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Section.swift; sourceTree = ""; }; 5BA9FD3D27FEF3C8002DE248 /* PreferencesTabViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesTabViewController.swift; sourceTree = ""; }; 5BA9FD8A28006B41002DE248 /* VDKComboBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VDKComboBox.swift; sourceTree = ""; }; + 5BAEFACF28012565001F42C9 /* mgrLangModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mgrLangModel.swift; sourceTree = ""; }; 5BB802D927FABA8300CF1C19 /* ctlInputMethod_Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ctlInputMethod_Menu.swift; sourceTree = ""; }; 5BBBB75D27AED54C0023B93A /* Beep.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = Beep.m4a; sourceTree = ""; }; 5BBBB75E27AED54C0023B93A /* Fart.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = Fart.m4a; sourceTree = ""; }; @@ -478,6 +480,7 @@ D41355D6278D7409005E5CBD /* mgrLangModel.h */, D495583A27A5C6C4006ADE1C /* mgrLangModel_Privates.h */, D41355D7278D7409005E5CBD /* mgrLangModel.mm */, + 5BAEFACF28012565001F42C9 /* mgrLangModel.swift */, 5B62A32527AE758000A19448 /* SubLanguageModels */, ); path = LangModelRelated; @@ -1086,6 +1089,7 @@ 5B62A32927AE77D100A19448 /* FSEventStreamHelper.swift in Sources */, D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */, 5B62A33627AE795800A19448 /* mgrPrefs.swift in Sources */, + 5BAEFAD028012565001F42C9 /* mgrLangModel.swift in Sources */, 5B62A33827AE79CD00A19448 /* NSStringUtils.swift in Sources */, 5BA9FD0F27FEDB6B002DE248 /* suiPrefPaneGeneral.swift in Sources */, 5BA9FD4927FEF3C9002DE248 /* Section.swift in Sources */,