Compare commits

...

81 Commits
3.8.0 ... main

Author SHA1 Message Date
ShikiSuen abb17e9ff6 [VersionUp] 3.8.5 GM Build 3850. 2024-04-07 00:56:09 +08:00
ShikiSuen afad2076b0 Update Data - 20240407 2024-04-07 00:26:41 +08:00
ShikiSuen 40245cf1d5 ToolTipUI // Enable round panel corners on old systems. 2024-04-06 17:07:13 +08:00
ShikiSuen 08a54c14c1 PCB // Enable round panel corners. 2024-04-06 16:38:26 +08:00
ShikiSuen 67a6bb5f87 LMMgr && Main // Allow dumping user dicts through terminal. 2024-04-02 19:12:35 +08:00
ShikiSuen 97a9e9aa5c LMAssembly // Let LMInstantiator summarize user data. 2024-04-02 19:12:35 +08:00
ShikiSuen c932083c5f LMInstantiator // Make async optional while loading user dicts. 2024-04-02 18:47:18 +08:00
ShikiSuen 25d8f7c093 LMInstantiator // Stop pinning default user weights for single reading. 2024-03-30 18:38:45 +08:00
ShikiSuen 14adf03311 LMInstantiator // Differentiate scores from factory results. 2024-03-30 18:38:45 +08:00
ShikiSuen 817df50916 LMMgr.UserPhrase // Fine-tweak suggestNextFreq(). 2024-03-30 18:31:06 +08:00
ShikiSuen e8961ff33f SessionCtl // Switch to .ofEmpty() state on toggling CapsLock. 2024-03-21 19:58:41 +08:00
ShikiSuen 2d23deb83a Tekkon // Update to v1.6.0 release. 2024-03-21 16:50:49 +08:00
ShikiSuen fc5243c97f AppDelegate // Update max RAM threshold to 1024MB. 2024-03-21 16:50:46 +08:00
ShikiSuen 9e1d130ba7 UserDef // +kMinCellWidthForHorizontalMatrix. 2024-03-10 22:00:51 +08:00
ShikiSuen 5a6aee2a25 PCB // Tweak panel opacity. 2024-03-10 22:00:51 +08:00
ShikiSuen aa4162fa9b DataCompiler // Fix SQLite random segmentation fault 11. 2024-03-10 00:03:54 +08:00
ShikiSuen 951f41461a TDKCandidates // Refactor highlightedColor(). 2024-03-09 04:14:59 +08:00
ShikiSuen f46cfda6f5 [VersionUp] 3.8.4 GM Build 3840. 2024-03-08 03:21:39 +08:00
ShikiSuen 03edccff4f Update Data - 20240308 2024-03-08 03:18:05 +08:00
ShikiSuen d8aba434d9 SecureEventInputSputnik // Patch a memory leak, etc. 2024-03-08 01:54:11 +08:00
ShikiSuen 005116c429 SPM // Consolidate dependencies. 2024-03-06 00:18:55 +08:00
ShikiSuen 93256f0095 Xcode // Add a debuggable-only target. 2024-03-04 18:34:28 +08:00
ShikiSuen 11bb5a9b66 Xcode // Stop stripping Swift symbols. 2024-03-04 17:32:25 +08:00
ShikiSuen 9dc7821708 [VersionUp] 3.8.3 GM Build 3830. 2024-03-02 23:06:53 +08:00
ShikiSuen 5d62d5b66d Update Data - 20240301 2024-03-02 23:06:53 +08:00
ShikiSuen c63c531f1b MainAssembly // Include remaining AppDelegate IBOutlets. 2024-03-02 23:06:53 +08:00
ShikiSuen 35d4426730 UserPhrase // Improve score boosting / nerfing for single kanji. 2024-03-02 23:06:53 +08:00
ShikiSuen b628ddd082 LMInstantiator // Expose factoryCoreUnigramsFor(). 2024-03-02 23:06:53 +08:00
ShikiSuen 4e00791144 LMPlainBopomofo // Fix mistakes in Eten DOS CHS Sequence Data. 2024-03-02 23:06:53 +08:00
ShikiSuen 1e098cac53 InputHandler // Prioritize the handling of the service menu. 2024-03-02 23:06:53 +08:00
ShikiSuen 0107e7cd78 ServiceMenu // Filter some services if readings are unavailable. 2024-03-02 23:06:53 +08:00
ShikiSuen 9411686d03 UserDef // +useShiftQuestionToCallServiceMenu. 2024-03-02 23:06:53 +08:00
ShikiSuen 5eec7cd604 MainAssembly // + Candidate Service (Menu & Editor). 2024-03-02 23:06:53 +08:00
ShikiSuen dc79c629a1 CandidateNode // Subclass: ServiceMenuNode. 2024-03-02 23:06:53 +08:00
ShikiSuen 040c597345 Shared // +CandidateTextService. 2024-03-02 23:06:53 +08:00
ShikiSuen 923471c8bb UserDef // +kCandidateServiceMenuContents. 2024-03-02 23:06:53 +08:00
ShikiSuen 46d4e7bdb3 MainAssembly // Refactor wherever using UniformTypeIdentifiers. 2024-03-02 23:06:53 +08:00
ShikiSuen 55dcdc8ce0 (NS)String // Add some codepoint extensions. 2024-03-02 23:06:53 +08:00
ShikiSuen bd5fdcaa26 ClientListMgr // Fix metrics and the invisible scroller. 2024-03-02 23:06:53 +08:00
ShikiSuen d5d9167b1e CocoaImpl // Fix isDarkMode(). 2024-03-02 23:06:53 +08:00
ShikiSuen c2679735c1 PrefMgr // Refactor the didSet methods. 2024-03-02 23:06:53 +08:00
ShikiSuen 4904664277 Shared // Fix a KVO Observer. 2024-03-02 23:06:53 +08:00
ShikiSuen 76dd75ce5a UserDef // +kSpecifyCmdOptCtrlEnterBehavior. 2024-03-02 23:06:53 +08:00
ShikiSuen 7be2a85b25 BrailleSputnik // Initial Implementation. 2024-03-02 23:06:53 +08:00
ShikiSuen 549c361af4 [VersionUp] 3.8.2 GM Build 3820. 2024-03-02 23:06:53 +08:00
ShikiSuen 72655119fa Update Data - 20240223 2024-02-24 03:56:21 +08:00
ShikiSuen 3ebb5f2f48 LMAssembly // Integrate EtenDOS SCPC data into the codebase. 2024-02-23 14:01:30 +08:00
ShikiSuen e44843e603 LMAssembly // Pack LMUserOverride inside LMInstantiator, etc. 2024-02-23 13:55:29 +08:00
ShikiSuen c5899152e6 InputHandler // Move some case-switch results to InputMode enum. 2024-02-21 14:58:26 +08:00
ShikiSuen 275288ea61 Hotenka // Deprecate NSJSONSerialization. 2024-02-19 01:33:47 +08:00
ShikiSuen 4184c3c1d2 DataCompiler // Post-dump SQLite database. 2024-02-18 23:58:06 +08:00
ShikiSuen a559818111 [VersionUp] 3.8.1 SP1 Build 3811. 2024-02-17 17:18:41 +08:00
ShikiSuen 9e3f0d7929 SessionCtl // Fix incorrect menu behavior. 2024-02-17 17:18:36 +08:00
ShikiSuen 3545cc1c22 AppInstaller // Add missing copyright label. 2024-02-17 16:20:59 +08:00
ShikiSuen 69d839e833 VwrAboutCocoa // Patch vertical button stack spacing for macOS 10.9. 2024-02-17 16:20:59 +08:00
ShikiSuen e887ba01a5 CtlCandidateTDK // Patch another issue with reverse lookup. 2024-02-17 16:20:59 +08:00
ShikiSuen b20dfec630 TDKCandidates // Force-refresh reverse lookup results on refresh. 2024-02-17 12:41:45 +08:00
ShikiSuen 63f7cc91fc [VersionUp] 3.8.1 GM Build 3810. 2024-02-17 00:37:24 +08:00
ShikiSuen b9595bed4f Update Data - 20240217 2024-02-17 00:09:04 +08:00
ShikiSuen 83b80fb863 AboutCocoa & ClientListMgr // Tweak metrics. 2024-02-17 00:07:46 +08:00
ShikiSuen 791256cf31 SessionCtl // Optimize the IME menu for macOS 10.9. 2024-02-17 00:07:46 +08:00
ShikiSuen 0e4651e70e UserDef // +filterNonCNSReadingsForCHTInput. 2024-02-16 16:18:34 +08:00
ShikiSuen 58815d7c54 LMAssembly // Implement CNS pronunciation filter. 2024-02-16 16:18:34 +08:00
ShikiSuen b479acf779 Repo // Add KimoDataReader using NSConnection. 2024-02-16 16:18:34 +08:00
ShikiSuen 424a736c8e TDKCandidates // Support displaying codepoints. 2024-02-15 13:44:25 +08:00
ShikiSuen ccd9b391e4 UserDef // +showCodePointInCandidateUI. 2024-02-15 13:44:25 +08:00
ShikiSuen 5090c7e6d4 CocoaExtension // Allow overriding stack spacing. 2024-02-14 22:36:54 +08:00
ShikiSuen 169902db19 SettingsCocoa // Simplify some phrases. 2024-02-14 22:15:49 +08:00
ShikiSuen 3af51f22e1 SettingsCocoa // Share metrics across panes. 2024-02-14 22:15:49 +08:00
ShikiSuen ad98484094
GitHub // Update CI to build SPM packages instead. 2024-02-14 17:54:10 +08:00
ShikiSuen 982686018b Makefile // Optimize for SPM packages. 2024-02-14 17:41:34 +08:00
ShikiSuen b013dc4d82 FileOpenMethod // Use localized app names. 2024-02-14 14:37:47 +08:00
ShikiSuen bb4729ee3f LMMgr // Fix when to reload phrase editors. 2024-02-14 13:44:22 +08:00
ShikiSuen 71e34790e8 SettingsCocoa // Add file drag receiver button for Kimo Data import. 2024-02-14 04:32:49 +08:00
ShikiSuen 54f61a28b1 Repo // Unify modal window calling methods. 2024-02-14 04:14:17 +08:00
ShikiSuen 21dcb58748 SettingsCocoa // Make narration settings effective immediately. 2024-02-13 20:53:34 +08:00
ShikiSuen 903faae51f SettingsCocoa // Lock dimensions for descriptions and titles. 2024-02-13 18:10:38 +08:00
ShikiSuen 820ee5b0f6 CocoaImpl // Fix NSView.makeSimpleConstraint(). 2024-02-13 18:10:38 +08:00
ShikiSuen 2ce79e5a05 SettingsCocoa // Also layout subtree at final step. 2024-02-13 18:10:38 +08:00
ShikiSuen 92b2ada9c5 LatinKeyboardMappings // Maintenance fix with Dvorak.QwertyCMD. 2024-02-13 18:10:38 +08:00
ShikiSuen e749e65627 Xcode // FUCK OFF USER SCRIPT SANDBOXING. 2024-02-13 00:19:42 +08:00
201 changed files with 14121 additions and 9136 deletions

View File

@ -1,4 +1,4 @@
name: Build-with-macOS-latest
name: debug-macOS-MainAssembly
on:
push:
branches: [ "main" ]
@ -17,6 +17,6 @@ jobs:
xcode-version: '^15.1'
- uses: actions/checkout@v1
- name: Clean
run: make clean
run: make spmClean
- name: Build
run: git pull --all && git submodule sync; make update; make
run: make spmDebug

View File

@ -194,7 +194,7 @@ func prepareDatabase() -> Bool {
PRIMARY KEY (theChar)
) WITHOUT ROWID;
"""
guard sqlite3_open(urlSQLite, &ptrSQL) == SQLITE_OK else { return false }
guard sqlite3_open(":memory:", &ptrSQL) == SQLITE_OK else { return false }
guard sqlite3_exec(ptrSQL, "PRAGMA synchronous = OFF;", nil, nil, nil) == SQLITE_OK else { return false }
guard sqlite3_exec(ptrSQL, "PRAGMA journal_mode = OFF;", nil, nil, nil) == SQLITE_OK else { return false }
guard sqlMakeTableMACV.runAsSQLExec(dbPointer: &ptrSQL) else { return false }
@ -231,6 +231,20 @@ func prepareDatabase() -> Bool {
return true
}
// MARK: - Dump SQLite3 Memory Database to File.
@discardableResult func dumpSQLDB() -> Bool {
var ptrSQLTarget: OpaquePointer?
defer { sqlite3_close_v2(ptrSQLTarget) }
guard sqlite3_open(urlSQLite, &ptrSQLTarget) == SQLITE_OK else { return false }
let ptrBackupObj = sqlite3_backup_init(ptrSQLTarget, "main", ptrSQL, "main")
if ptrBackupObj != nil {
sqlite3_backup_step(ptrBackupObj, -1)
sqlite3_backup_finish(ptrBackupObj)
}
return sqlite3_errcode(ptrSQLTarget) == SQLITE_OK
}
// MARK: -
func rawDictForPhrases(isCHS: Bool) -> [Unigram] {
@ -1044,6 +1058,19 @@ func healthCheck(_ data: [Unigram]) -> String {
return result
}
// MARK: - Flags
struct TaskFlags: OptionSet {
public let rawValue: Int
public init(rawValue: Int) {
self.rawValue = rawValue
}
public static let common = TaskFlags(rawValue: 1 << 0)
public static let chs = TaskFlags(rawValue: 1 << 1)
public static let cht = TaskFlags(rawValue: 1 << 2)
}
// MARK: -
var compileJSON = false
@ -1066,57 +1093,71 @@ func main() {
NSLog("// SQLite 資料庫初期化失敗。")
exit(-1)
}
var taskFlags: TaskFlags = [.common, .chs, .cht] {
didSet {
guard taskFlags.isEmpty else { return }
NSLog("// 全部 TXT 辭典檔案建置完畢。")
if compileJSON {
NSLog("// 全部 JSON 辭典檔案建置完畢。")
}
if compileSQLite, prepared {
NSLog("// 開始整合反查資料。")
mapReverseLookupForCheck.forEach { key, values in
values.reversed().forEach { valueLiteral in
let value = cnvPhonabetToASCII(valueLiteral)
if !rangeMapReverseLookup[key, default: []].contains(value) {
rangeMapReverseLookup[key, default: []].insert(value, at: 0)
}
}
}
NSLog("// 反查資料整合完畢。")
NSLog("// 準備建置 SQL 資料庫。")
writeMainMapToSQL(rangeMapJSONCHS, column: "theDataCHS")
writeMainMapToSQL(rangeMapJSONCHT, column: "theDataCHT")
writeMainMapToSQL(rangeMapSymbols, column: "theDataSYMB")
writeMainMapToSQL(rangeMapZhuyinwen, column: "theDataCHEW")
writeMainMapToSQL(rangeMapCNS, column: "theDataCNS")
writeRevLookupMapToSQL(rangeMapReverseLookup)
let committed = "commit;".runAsSQLExec(dbPointer: &ptrSQL)
assert(committed)
let compressed = "VACUUM;".runAsSQLExec(dbPointer: &ptrSQL)
assert(compressed)
if !dumpSQLDB() {
NSLog("// SQLite 辭典傾印失敗。")
} else {
NSLog("// 全部 SQLite 辭典檔案建置完畢。")
}
sqlite3_close_v2(ptrSQL)
}
}
}
let globalQueue = DispatchQueue.global(qos: .default)
let group = DispatchGroup()
group.enter()
globalQueue.async {
NSLog("// 準備編譯符號表情ㄅ文語料檔案。")
commonFileOutput()
taskFlags.remove(.common)
group.leave()
}
group.enter()
globalQueue.async {
NSLog("// 準備編譯繁體中文核心語料檔案。")
fileOutput(isCHS: false)
taskFlags.remove(.cht)
group.leave()
}
group.enter()
globalQueue.async {
NSLog("// 準備編譯簡體中文核心語料檔案。")
fileOutput(isCHS: true)
taskFlags.remove(.chs)
group.leave()
}
//
group.wait()
NSLog("// 全部 TXT 辭典檔案建置完畢。")
if compileJSON {
NSLog("// 全部 JSON 辭典檔案建置完畢。")
}
if compileSQLite, prepared {
NSLog("// 開始整合反查資料。")
mapReverseLookupForCheck.forEach { key, values in
values.reversed().forEach { valueLiteral in
let value = cnvPhonabetToASCII(valueLiteral)
if !rangeMapReverseLookup[key, default: []].contains(value) {
rangeMapReverseLookup[key, default: []].insert(value, at: 0)
}
}
}
NSLog("// 反查資料整合完畢。")
NSLog("// 準備建置 SQL 資料庫。")
writeMainMapToSQL(rangeMapJSONCHS, column: "theDataCHS")
writeMainMapToSQL(rangeMapJSONCHT, column: "theDataCHT")
writeMainMapToSQL(rangeMapSymbols, column: "theDataSYMB")
writeMainMapToSQL(rangeMapZhuyinwen, column: "theDataCHEW")
writeMainMapToSQL(rangeMapCNS, column: "theDataCNS")
writeRevLookupMapToSQL(rangeMapReverseLookup)
let committed = "commit;".runAsSQLExec(dbPointer: &ptrSQL)
assert(committed)
let compressed = "VACUUM;".runAsSQLExec(dbPointer: &ptrSQL)
assert(compressed)
sqlite3_close_v2(ptrSQL)
NSLog("// 全部 SQLite 辭典檔案建置完畢。")
}
}
main()

View File

@ -10,6 +10,8 @@ import AppKit
import SwiftUI
public struct MainView: View {
static let strCopyrightLabel = Bundle.main.localizedInfoDictionary?["NSHumanReadableCopyright"] as? String ?? "BAD_COPYRIGHT_LABEL"
@State var pendingSheetPresenting = false
@State var isShowingAlertForFailedInstallation = false
@State var isShowingAlertForMissingPostInstall = false
@ -53,6 +55,7 @@ public struct MainView: View {
Text("v\(versionString) Build \(installingVersion)").lineLimit(1)
}.fixedSize()
Text("i18n:installer.APP_DERIVED_FROM").font(.custom("Tahoma", size: 11))
Text(Self.strCopyrightLabel).font(.custom("Tahoma", size: 11))
Text("i18n:installer.DEV_CREW").font(.custom("Tahoma", size: 11)).padding([.vertical], 2)
}
}

View File

@ -11,6 +11,24 @@ BUILD_SETTINGS += ARCHS="$(ARCHS)"
BUILD_SETTINGS += ONLY_ACTIVE_ARCH=NO
endif
spmDebug:
swift build -c debug --package-path ./Packages/vChewing_MainAssembly/
spmRelease:
swift build -c release --package-path ./Packages/vChewing_MainAssembly/
spmLintFormat:
make lint --file=./Packages/Makefile || true
make format --file=./Packages/Makefile || true
spmClean:
@for currentDir in $$(ls ./Packages/); do \
if [ -d $$a ]; then \
echo "processing folder $$currentDir"; \
swift package clean --package-path ./Packages/$$currentDir || true; \
fi; \
done;
release:
xcodebuild -project vChewing.xcodeproj -scheme vChewingInstaller -configuration Release $(BUILD_SETTINGS) build
@ -42,6 +60,7 @@ install-release: permission-check
.PHONY: clean
clean:
make clean --file=./Packages/Makefile || true
xcodebuild -scheme vChewingInstaller -configuration Debug $(BUILD_SETTINGS) clean
xcodebuild -scheme vChewingInstaller -configuration Release $(BUILD_SETTINGS) clean
make clean --file=./Source/Data/Makefile || true

1
Packages/.clang-format Normal file
View File

@ -0,0 +1 @@
BasedOnStyle: Microsoft

104
Packages/.swiftformat.json Normal file
View File

@ -0,0 +1,104 @@
# SwiftFormat config compliant with Google Swift Guideline
# https://google.github.io/swift/#control-flow-statements
# Specify version used in a project
--swiftversion 5.5
# Rules explicitly required by the guideline
--rules \
blankLinesAroundMark, \
blankLinesAtEndOfScope, \
blankLinesAtStartOfScope, \
blankLinesBetweenScopes, \
braces, \
consecutiveBlankLines, \
consecutiveSpaces, \
duplicateImports, \
elseOnSameLine, \
emptyBraces, \
enumNamespaces, \
extensionAccessControl, \
hoistPatternLet, \
indent, \
leadingDelimiters, \
linebreakAtEndOfFile, \
markTypes, \
organizeDeclarations, \
redundantInit, \
redundantParens, \
redundantPattern, \
redundantRawValues, \
redundantType, \
redundantVoidReturnType, \
semicolons, \
sortedImports, \
sortedSwitchCases, \
spaceAroundBraces, \
spaceAroundBrackets, \
spaceAroundComments, \
spaceAroundGenerics, \
spaceAroundOperators, \
spaceAroundParens, \
spaceInsideBraces, \
spaceInsideBrackets, \
spaceInsideComments, \
spaceInsideGenerics, \
spaceInsideParens, \
todos, \
trailingClosures, \
trailingCommas, \
trailingSpace, \
typeSugar, \
void, \
wrap, \
wrapArguments, \
wrapAttributes, \
#
#
# Additional rules not mentioned in the guideline, but helping to keep the codebase clean
# Quoting the guideline:
# Common themes among the rules in this section are:
# avoid redundancy, avoid ambiguity, and prefer implicitness over explicitness
# unless being explicit improves readability and/or reduces ambiguity.
#
#
andOperator, \
isEmpty, \
redundantBackticks, \
redundantBreak, \
redundantExtensionACL, \
redundantGet, \
redundantLetError, \
redundantNilInit, \
redundantObjc, \
redundantReturn, \
redundantSelf, \
strongifiedSelf
# Options for basic rules
--extensionacl on-declarations
--funcattributes prev-line
--indent 2
--maxwidth 100
--typeattributes prev-line
--varattributes prev-line
--voidtype tuple
--wraparguments before-first
--wrapparameters before-first
--wrapcollections before-first
--wrapreturntype if-multiline
--wrapconditions after-first
# Option for additional rules
--self init-only
# Excluded folders
--exclude Pods,**/UNTESTED_TODO,vendor,fastlane
# https://github.com/NoemiRozpara/Google-SwiftFormat-Config

View File

@ -13,13 +13,13 @@ let package = Package(
),
],
dependencies: [
.package(path: "../vChewing_CocoaExtension"),
.package(path: "../vChewing_OSFrameworkImpl"),
],
targets: [
.target(
name: "NSAttributedTextView",
dependencies: [
.product(name: "CocoaExtension", package: "vChewing_CocoaExtension"),
.product(name: "OSFrameworkImpl", package: "vChewing_OSFrameworkImpl"),
]
),
.testTarget(

View File

@ -6,7 +6,7 @@
// Modified by The vChewing Project in order to use it with AppKit.
import AppKit
import CocoaExtension
import OSFrameworkImpl
import SwiftUI
@available(macOS 10.15, *)

View File

@ -7,9 +7,9 @@
// requirements defined in MIT License.
import AppKit
import CocoaExtension
import Foundation
@testable import NSAttributedTextView
import OSFrameworkImpl
import Shared
import XCTest

29
Packages/Makefile Normal file
View File

@ -0,0 +1,29 @@
+.PHONY: all
all: debug
debug:
swift build -c debug --package-path ./vChewing_MainAssembly/
release:
swift build -c release --package-path ./vChewing_MainAssembly/
clean:
@for currentDir in $$(ls ./); do \
if [ -d $$a ]; then \
echo "processing folder $$currentDir"; \
swift package clean --package-path ./$$currentDir || true; \
fi; \
done;
.PHONY: lint format
lintFormat: lint format
format:
@swiftformat --swiftversion 5.5 --indent 2 ./
lint:
@git ls-files --exclude-standard | grep -E '\.swift$$' | swiftlint --fix --autocorrect
.PHONY: permission-check install-debug install-release

View File

@ -1,7 +1,6 @@
// (c) 2019 and onwards Robert Muckle-Jones (Apache 2.0 License).
import Foundation
import SwiftExtension
public class LineReader {
let encoding: String.Encoding

View File

@ -1,9 +1,8 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

View File

@ -0,0 +1,35 @@
// swift-tools-version: 5.7
import PackageDescription
let package = Package(
name: "BrailleSputnik",
platforms: [
.macOS(.v11),
],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "BrailleSputnik",
targets: ["BrailleSputnik"]
),
],
dependencies: [
.package(path: "../vChewing_Shared"),
.package(path: "../vChewing_Tekkon"),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "BrailleSputnik",
dependencies: [
.product(name: "Shared", package: "vChewing_Shared"),
.product(name: "Tekkon", package: "vChewing_Tekkon"),
]
),
.testTarget(
name: "BrailleSputnikTests",
dependencies: ["BrailleSputnik"]
),
]
)

View File

@ -0,0 +1,284 @@
// (c) 2022 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
extension BrailleSputnik {
enum Braille: String {
case blank = "" // U+2800
case d1 = ""
case d2 = ""
case d12 = ""
case d3 = ""
case d13 = ""
case d23 = ""
case d123 = ""
case d4 = ""
case d14 = ""
case d24 = ""
case d124 = ""
case d34 = ""
case d134 = ""
case d234 = ""
case d1234 = ""
case d5 = ""
case d15 = ""
case d25 = ""
case d125 = ""
case d35 = ""
case d135 = ""
case d235 = ""
case d1235 = ""
case d45 = ""
case d145 = ""
case d245 = ""
case d1245 = ""
case d345 = ""
case d1345 = ""
case d2345 = ""
case d12345 = ""
case d6 = ""
case d16 = ""
case d26 = ""
case d126 = ""
case d36 = ""
case d136 = ""
case d236 = ""
case d1236 = ""
case d46 = ""
case d146 = ""
case d246 = ""
case d1246 = ""
case d346 = ""
case d1346 = ""
case d2346 = ""
case d12346 = ""
case d56 = ""
case d156 = ""
case d256 = ""
case d1256 = ""
case d356 = ""
case d1356 = ""
case d2356 = ""
case d12356 = ""
case d456 = ""
case d1456 = ""
case d2456 = ""
case d12456 = ""
case d3456 = ""
case d13456 = ""
case d23456 = ""
case d123456 = ""
}
public enum BrailleStandard: Int {
case of1947 = 1
case of2018 = 2
}
}
protocol BrailleProcessingUnit {
var mapConsonants: [String: String] { get }
var mapSemivowels: [String: String] { get }
var mapVowels: [String: String] { get }
var mapIntonations: [String: String] { get }
var mapIntonationSpecialCases: [String: String] { get }
var mapCombinedVowels: [String: String] { get }
var mapPunctuations: [String: String] { get }
func handleSpecialCases(target: inout String, value: String?) -> Bool
}
// MARK: - Static Data conforming to 1947 Standard.
extension BrailleSputnik {
class BrailleProcessingUnit1947: BrailleProcessingUnit {
func handleSpecialCases(target _: inout String, value _: String?) -> Bool {
//
false
}
let mapConsonants: [String: String] = [
"": "", "": "", "": "",
"": "", "": "", "": "",
"": "", "": "", "": "",
"": "", "": "", "": "",
"": "", "": "", "": "",
"": "", "": "", "": "",
"": "", "": "", "": "",
]
let mapSemivowels: [String: String] = [
"": "", "": "", "": "",
]
let mapVowels: [String: String] = [
"": "", "": "", "": "",
"": "", "": "", "": "",
"": "", "": "", "": "",
"": "", "": "", "": "",
]
let mapIntonations: [String: String] = [
"˙": "⠱⠁", "ˇ": "", "ˊ": "", " ": "", "ˋ": "",
]
let mapIntonationSpecialCases: [String: String] = [
"ㄜ˙": "⠮⠁", "ㄚ˙": "⠜⠁", "ㄛ˙": "⠣⠁", "ㄣ˙": "⠥⠁",
]
let mapCombinedVowels: [String: String] = [
"ㄧㄝ": "", "ㄧㄣ": "", "ㄩㄝ": "",
"ㄨㄟ": "", "ㄨㄥ": "", "ㄨㄣ": "",
"ㄨㄚ": "", "ㄧㄡ": "", "ㄧㄤ": "",
"ㄧㄚ": "", "ㄨㄛ": "", "ㄧㄥ": "",
"ㄨㄞ": "", "ㄩㄥ": "", "ㄧㄠ": "",
"ㄧㄞ": "", "ㄨㄤ": "", "ㄩㄣ": "",
"ㄧㄢ": "", "ㄩㄢ": "", "ㄨㄢ": "",
]
let mapPunctuations: [String: String] = [
"": "⠤⠀", "·": "⠤⠀", "": "", "": "",
"": "", "": "⠕⠀", "": "⠇⠀", "": "⠒⠒",
"╴╴": "⠰⠰", "": "⠠⠤", "……": "⠐⠐⠐",
"": "⠐⠂", "—— ——": "⠐⠂⠐⠂", "": "⠈⠼", "": "⠪⠕",
"": "⠦⠦", "": "⠴⠴", "": "⠰⠤", "": "⠤⠆",
"": "⠦⠦", "": "⠴⠴", "": "⠰⠤", "": "⠤⠆",
"": "", "": "", "": "", "": "",
"": "", "": "", "": "", "": "",
]
}
}
// MARK: - Static Data conforming to 2018 Standard (GF0019-2018)
extension BrailleSputnik {
class BrailleProcessingUnit2018: BrailleProcessingUnit {
func handleSpecialCases(target: inout String, value: String?) -> Bool {
guard let value = value else { return false }
switch value {
case "": target = Braille.d2345.rawValue + Braille.d35.rawValue
case "": target = Braille.d4.rawValue + Braille.d2345.rawValue + Braille.d35.rawValue
default: return false
}
return true
}
let mapConsonants: [String: String] = [
"": Braille.d12.rawValue,
"": Braille.d1234.rawValue,
"": Braille.d134.rawValue,
"": Braille.d124.rawValue,
"": Braille.d145.rawValue,
"": Braille.d2345.rawValue,
"": Braille.d1345.rawValue,
"": Braille.d123.rawValue,
"": Braille.d1245.rawValue,
"": Braille.d13.rawValue,
"": Braille.d125.rawValue,
"": Braille.d1245.rawValue,
"": Braille.d13.rawValue,
"": Braille.d125.rawValue,
"": Braille.d34.rawValue,
"": Braille.d12345.rawValue,
"": Braille.d156.rawValue,
"": Braille.d245.rawValue,
"": Braille.d1356.rawValue,
"": Braille.d14.rawValue,
"": Braille.d234.rawValue,
]
let mapSemivowels: [String: String] = [
"": Braille.d24.rawValue,
"": Braille.d136.rawValue,
"": Braille.d346.rawValue,
]
let mapVowels: [String: String] = [
"": Braille.d35.rawValue,
"": Braille.d26.rawValue,
"": Braille.d26.rawValue,
"": Braille.d246.rawValue,
"": Braille.d2346.rawValue,
"": Braille.d235.rawValue,
"": Braille.d12356.rawValue,
"": Braille.d1236.rawValue,
"": Braille.d356.rawValue,
"": Braille.d236.rawValue,
"": Braille.d3456.rawValue, //
"": Braille.d1235.rawValue,
]
let mapIntonations: [String: String] = [
" ": Braille.d1.rawValue,
"ˊ": Braille.d2.rawValue,
"ˇ": Braille.d3.rawValue,
"ˋ": Braille.d23.rawValue,
// "˙": nil, //
]
let mapIntonationSpecialCases: [String: String] = [:]
let mapCombinedVowels: [String: String] = [
"ㄧㄚ": Braille.d1246.rawValue,
"ㄧㄝ": Braille.d15.rawValue,
"ㄧㄞ": Braille.d1246.rawValue, //
"ㄧㄠ": Braille.d345.rawValue,
"ㄧㄡ": Braille.d1256.rawValue,
"ㄧㄢ": Braille.d146.rawValue,
"ㄧㄣ": Braille.d126.rawValue,
"ㄧㄤ": Braille.d1346.rawValue,
"ㄧㄥ": Braille.d16.rawValue,
"ㄨㄚ": Braille.d123456.rawValue,
"ㄨㄛ": Braille.d135.rawValue,
"ㄨㄞ": Braille.d13456.rawValue,
"ㄨㄟ": Braille.d2456.rawValue,
"ㄨㄢ": Braille.d12456.rawValue,
"ㄨㄣ": Braille.d25.rawValue,
"ㄨㄤ": Braille.d2356.rawValue,
"ㄨㄥ": Braille.d256.rawValue,
"ㄩㄝ": Braille.d23456.rawValue,
"ㄩㄢ": Braille.d12346.rawValue,
"ㄩㄣ": Braille.d456.rawValue,
"ㄩㄥ": Braille.d1456.rawValue,
]
let mapPunctuations: [String: String] = [
"": Braille.d5.rawValue + Braille.d23.rawValue,
"·": Braille.d6.rawValue + Braille.d3.rawValue,
"": Braille.d5.rawValue,
"": Braille.d56.rawValue,
"": Braille.d4.rawValue,
"": Braille.d5.rawValue + Braille.d3.rawValue,
"": Braille.d56.rawValue + Braille.d2.rawValue,
"": Braille.d36.rawValue,
"——": Braille.d6.rawValue + Braille.d36.rawValue,
"……": Braille.d5.rawValue + Braille.d5.rawValue + Braille.d5.rawValue,
"-": Braille.d36.rawValue,
"": Braille.d5.rawValue, //
"": Braille.d2356.rawValue + Braille.d35.rawValue,
"": Braille.d5.rawValue + Braille.d36.rawValue,
"": Braille.d36.rawValue + Braille.d2.rawValue,
"": Braille.d5.rawValue + Braille.d3.rawValue,
"": Braille.d6.rawValue + Braille.d2.rawValue,
"": Braille.d45.rawValue + Braille.d45.rawValue,
"": Braille.d45.rawValue + Braille.d45.rawValue,
"": Braille.d45.rawValue,
"": Braille.d45.rawValue,
"": Braille.d45.rawValue + Braille.d45.rawValue,
"": Braille.d45.rawValue + Braille.d45.rawValue,
"": Braille.d45.rawValue,
"": Braille.d45.rawValue,
"": Braille.d56.rawValue + Braille.d3.rawValue,
"": Braille.d6.rawValue + Braille.d23.rawValue,
"": Braille.d56.rawValue + Braille.d23.rawValue,
"": Braille.d56.rawValue + Braille.d23.rawValue,
"": Braille.d56.rawValue + Braille.d23.rawValue,
"": Braille.d56.rawValue + Braille.d23.rawValue,
// "": "", "": "", // 2018
]
}
}

View File

@ -0,0 +1,109 @@
// (c) 2022 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import Tekkon
public class BrailleSputnik {
public var standard: BrailleStandard
public init(standard: BrailleStandard) {
self.standard = standard
}
var staticData: BrailleProcessingUnit {
switch standard {
case .of1947: return Self.staticData1947
case .of2018: return Self.staticData2018
}
}
static var sharedComposer = Tekkon.Composer("", arrange: .ofDachen, correction: true)
private static let staticData1947: BrailleProcessingUnit = BrailleProcessingUnit1947()
private static let staticData2018: BrailleProcessingUnit = BrailleProcessingUnit2018()
}
public extension BrailleSputnik {
func convertToBraille(smashedPairs: [(key: String, value: String)], extraInsertion: (reading: String, cursor: Int)? = nil) -> String {
var convertedStack: [String?] = []
var processedKeysCount = 0
var extraInsertion = extraInsertion
smashedPairs.forEach { key, value in
let subKeys = key.split(separator: "\t")
switch subKeys.count {
case 0: return
case 1:
guard !key.isEmpty else { break }
let isPunctuation: Bool = key.first == "_" //
if isPunctuation {
convertedStack.append(convertPunctuationToBraille(value))
} else {
var key = key.description
fixToneOne(target: &key)
convertedStack.append(convertPhonabetReadingToBraille(key, value: value))
}
processedKeysCount += 1
default:
//
subKeys.forEach { subKey in
var subKey = subKey.description
fixToneOne(target: &subKey)
convertedStack.append(convertPhonabetReadingToBraille(subKey))
processedKeysCount += 1
}
}
if let theExtraInsertion = extraInsertion, processedKeysCount == theExtraInsertion.cursor {
convertedStack.append(convertPhonabetReadingToBraille(theExtraInsertion.reading))
extraInsertion = nil
}
}
return convertedStack.compactMap(\.?.description).joined()
}
private func fixToneOne(target key: inout String) {
for char in key {
guard Tekkon.Phonabet(char.description).type != .null else { return }
}
if let lastChar = key.last?.description, Tekkon.Phonabet(lastChar).type != .intonation {
key += " "
}
}
func convertPunctuationToBraille(_ givenTarget: any StringProtocol) -> String? {
staticData.mapPunctuations[givenTarget.description]
}
func convertPhonabetReadingToBraille(_ rawReading: any StringProtocol, value referredValue: String? = nil) -> String? {
var resultStack = ""
//
guard !staticData.handleSpecialCases(target: &resultStack, value: referredValue) else { return resultStack }
Self.sharedComposer.clear()
rawReading.forEach { char in
Self.sharedComposer.receiveKey(fromPhonabet: char.description)
}
let consonant = Self.sharedComposer.consonant.value
let semivowel = Self.sharedComposer.semivowel.value
let vowel = Self.sharedComposer.vowel.value
let intonation = Self.sharedComposer.intonation.value
if !consonant.isEmpty {
resultStack.append(staticData.mapConsonants[consonant] ?? "")
}
let combinedVowels = Self.sharedComposer.semivowel.value + Self.sharedComposer.vowel.value
if combinedVowels.count == 2 {
resultStack.append(staticData.mapCombinedVowels[combinedVowels] ?? "")
} else {
resultStack.append(staticData.mapSemivowels[semivowel] ?? "")
resultStack.append(staticData.mapVowels[vowel] ?? "")
}
// 調
if let intonationSpecialCaseMetResult = staticData.mapIntonationSpecialCases[vowel + intonation] {
resultStack.append(intonationSpecialCaseMetResult.last?.description ?? "")
} else {
resultStack.append(staticData.mapIntonations[intonation] ?? "")
}
return resultStack
}
}

View File

@ -0,0 +1,28 @@
// (c) 2022 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
@testable import BrailleSputnik
import XCTest
final class BrailleSputnikTests: XCTestCase {
func testBrailleConversion() throws {
//
var rawReadingStr = "ㄉㄚˋ-ㄑㄧㄡ-ㄑㄧㄡ-ㄅㄧㄥˋ-ㄌㄜ˙-ㄦˋ-ㄑㄧㄡ-ㄑㄧㄡ-ㄑㄧㄠˊ-_"
rawReadingStr += "-ㄙㄢ-ㄑㄧㄡ-ㄑㄧㄡ-ㄘㄞˇ-ㄧㄠˋ-ㄙˋ-ㄑㄧㄡ-ㄑㄧㄡ-ㄠˊ-_。"
let rawReadingArray: [(key: String, value: String)] = rawReadingStr.split(separator: "-").map {
let value: String = $0.first == "_" ? $0.last?.description ?? "" : ""
return (key: $0.description, value: value)
}
let processor = BrailleSputnik(standard: .of1947)
let result1947 = processor.convertToBraille(smashedPairs: rawReadingArray)
XCTAssertEqual(result1947, "⠙⠜⠐⠚⠎⠄⠚⠎⠄⠕⠽⠐⠉⠮⠁⠱⠐⠚⠎⠄⠚⠎⠄⠚⠪⠂⠆⠑⠧⠄⠚⠎⠄⠚⠎⠄⠚⠺⠈⠪⠐⠑⠐⠚⠎⠄⠚⠎⠄⠩⠂⠤⠀")
processor.standard = .of2018
let result2018 = processor.convertToBraille(smashedPairs: rawReadingArray)
XCTAssertEqual(result2018, "⠙⠔⠆⠅⠳⠁⠅⠳⠁⠃⠡⠆⠇⠢⠗⠆⠅⠳⠁⠅⠳⠁⠅⠜⠂⠐⠎⠧⠁⠅⠳⠁⠅⠳⠁⠉⠪⠄⠜⠆⠎⠆⠅⠳⠁⠅⠳⠁⠖⠂⠐⠆")
}
}

View File

@ -19,6 +19,7 @@ public class CandidateCellData: Hashable {
public static var unifiedSize: Double = 16
public static var unifiedCharDimension: Double { ceil(unifiedSize * 1.0125 + 7) }
public static var unifiedTextHeight: Double { ceil(unifiedSize * 19 / 16) }
static var internalPrefs = PrefMgr()
public var selectionKey: String
public let displayedText: String
public private(set) var textDimension: NSSize
@ -81,7 +82,8 @@ public class CandidateCellData: Hashable {
}
public func cellLength(isMatrix: Bool = true) -> Double {
let minLength = ceil(Self.unifiedCharDimension * 2 + size * 1.25)
let factor: CGFloat = (Self.internalPrefs.minCellWidthForHorizontalMatrix == 0) ? 1.5 : 2
let minLength = ceil(Self.unifiedCharDimension * factor + size * 1.25)
if displayedText.count <= 2, isMatrix { return minLength }
return textDimension.width
}
@ -200,14 +202,14 @@ public class CandidateCellData: Hashable {
return attrStrCandidate
}
public var charDescriptions: [String] {
public func charDescriptions(shortened: Bool = false) -> [String] {
var result = displayedText
if displayedText.contains("("), displayedText.count > 2 {
result = displayedText.replacingOccurrences(of: "(", with: "").replacingOccurrences(of: ")", with: "")
}
return result.flatMap(\.unicodeScalars).compactMap {
let theName: String = $0.properties.name ?? ""
return String(format: "U+%02X %@", $0.value, theName)
return shortened ? String(format: "U+%02X", $0.value) : String(format: "U+%02X %@", $0.value, theName)
}
}

View File

@ -301,10 +301,12 @@ extension CandidatePool {
.font: Self.blankCell.phraseFont(size: reverseLookupTextSize),
]
let result = NSMutableAttributedString(string: "", attributes: attrReverseLookupSpacer)
var addedCounter = 0
for neta in reverseLookupResult {
result.append(NSAttributedString(string: " ", attributes: attrReverseLookupSpacer))
result.append(NSAttributedString(string: " \(neta) ", attributes: attrReverseLookup))
if maxLinesPerPage == 1 { break }
addedCounter += 1
if maxLinesPerPage == 1, addedCounter == 2 { break }
}
return result
}

View File

@ -17,26 +17,24 @@ open class CtlCandidate: NSWindowController, CtlCandidateProtocol {
open var reverseLookupResult: [String] = []
open func highlightedColor() -> NSColor {
var result = NSColor.controlAccentColor
var colorBlendAmount: Double = NSApplication.isDarkMode ? 0.3 : 0.0
if #available(macOS 10.14, *), !NSApplication.isDarkMode, locale == "zh-Hant" {
colorBlendAmount = 0.15
var result = NSColor.clear
if #available(macOS 10.14, *) {
result = .controlAccentColor
} else {
result = .alternateSelectedControlTextColor
}
let colorBlendAmount = 0.3
//
switch locale {
case "zh-Hans":
result = NSColor.systemRed
result = NSColor.red
case "zh-Hant":
result = NSColor.systemBlue
result = NSColor.blue
case "ja":
result = NSColor.systemBrown
result = NSColor.brown
default: break
}
var blendingAgainstTarget: NSColor = NSApplication.isDarkMode ? NSColor.black : NSColor.white
if #unavailable(macOS 10.14) {
colorBlendAmount = 0.3
blendingAgainstTarget = NSColor.white
}
let blendingAgainstTarget: NSColor = NSApplication.isDarkMode ? NSColor.black : NSColor.white
return result.blended(withFraction: colorBlendAmount, of: blendingAgainstTarget)!
}

View File

@ -7,7 +7,7 @@
// requirements defined in MIT License.
import AppKit
import CocoaExtension
import OSFrameworkImpl
import Shared
private extension NSUserInterfaceLayoutOrientation {
@ -108,16 +108,34 @@ public class CtlCandidateTDK: CtlCandidate, NSWindowDelegate {
override open func updateDisplay() {
guard let window = window else { return }
if let currentCandidateText = Self.thePool.currentSelectedCandidateText {
reverseLookupResult = delegate?.reverseLookup(for: currentCandidateText) ?? []
Self.thePool.reverseLookupResult = reverseLookupResult
Self.thePool.tooltip = delegate?.candidateToolTip(shortened: !Self.thePool.isMatrix) ?? ""
}
delegate?.candidatePairHighlightChanged(at: highlightedIndex)
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.updateNSWindowModern(window)
}
//
reverseLookupResult = []
//
if let currentCandidate = Self.thePool.currentCandidate {
let displayedText = currentCandidate.displayedText
var lookupResult: [String?] = delegate?.reverseLookup(for: displayedText) ?? []
if displayedText.count == 1, delegate?.showCodePointForCurrentCandidate ?? false {
if lookupResult.isEmpty {
lookupResult.append(currentCandidate.charDescriptions(shortened: !Self.thePool.isMatrix).first)
} else {
lookupResult.insert(currentCandidate.charDescriptions(shortened: true).first, at: lookupResult.startIndex)
}
reverseLookupResult = lookupResult.compactMap { $0 }
} else {
reverseLookupResult = lookupResult.compactMap { $0 }
// UNICODE
if !Self.thePool.isMatrix {
reverseLookupResult = [reverseLookupResult.first].compactMap { $0 }
}
}
}
Self.thePool.reverseLookupResult = reverseLookupResult
Self.thePool.tooltip = delegate?.candidateToolTip(shortened: !Self.thePool.isMatrix) ?? ""
delegate?.candidatePairHighlightChanged(at: highlightedIndex)
}
func updateNSWindowModern(_ window: NSWindow) {

View File

@ -7,7 +7,7 @@
// requirements defined in MIT License.
import AppKit
import CocoaExtension
import OSFrameworkImpl
import Shared
/// AppKit SwiftUI

View File

@ -100,9 +100,7 @@ public class HotenkaChineseConverter {
dictFiles = .init()
do {
let rawData = try Data(contentsOf: URL(fileURLWithPath: jsonDir))
guard let rawJSON: [String: [String: String]] = try JSONSerialization.jsonObject(with: rawData) as? [String: [String: String]] else {
throw NSError()
}
let rawJSON = try JSONDecoder().decode([String: [String: String]].self, from: rawData)
dict = rawJSON
} catch {
NSLog("// Exception happened when reading dict json at: \(jsonDir).")

View File

@ -40,7 +40,10 @@ extension HotenkaTests {
let testInstance: HotenkaChineseConverter = .init(dictDir: testDataPath)
NSLog("// Loading complete. Generating json dict file.")
do {
try JSONSerialization.data(withJSONObject: testInstance.dict, options: .sortedKeys).write(to: URL(fileURLWithPath: testDataPath + "convdict.json"))
let urlOutput = URL(fileURLWithPath: testDataPath + "convdict.json")
let encoder = JSONEncoder()
encoder.outputFormatting = .sortedKeys
try encoder.encode(testInstance.dict).write(to: urlOutput, options: .atomic)
} catch {
NSLog("// Error on writing strings to file: \(error)")
}

View File

@ -15,23 +15,13 @@ public enum IMKHelper {
///
/// SwiftUI 便使
public static let arrWhitelistedKeyLayoutsASCII: [String] = {
var result = [
"com.apple.keylayout.ABC",
"com.apple.keylayout.ABC-AZERTY",
"com.apple.keylayout.ABC-QWERTZ",
"com.apple.keylayout.British",
"com.apple.keylayout.Colemak",
"com.apple.keylayout.Dvorak",
"com.apple.keylayout.Dvorak-Left",
"com.apple.keylayout.DVORAK-QWERTYCMD",
"com.apple.keylayout.Dvorak-Right",
]
if #unavailable(macOS 10.13) {
result.insert("com.apple.keylayout.US", at: result.startIndex)
result.append("com.apple.keylayout.German")
result.append("com.apple.keylayout.French")
var results = LatinKeyboardMappings.allCases
if #available(macOS 10.13, *) {
results = results.filter {
![.qwertyUS, .qwertzGerman, .azertyFrench].contains($0)
}
}
return result
return results.map(\.rawValue)
}()
public static let arrDynamicBasicKeyLayouts: [String] = [

View File

@ -8,25 +8,27 @@
import Foundation
public enum LatinKeyboardMappings: String {
public enum LatinKeyboardMappings: String, CaseIterable {
case qwerty = "com.apple.keylayout.ABC"
case qwertyUS = "com.apple.keylayout.US"
case qwertyBritish = "com.apple.keylayout.British"
case qwertyUS = "com.apple.keylayout.US" // 10.9 - 10.12
case azerty = "com.apple.keylayout.ABC-AZERTY"
case azertyFrench = "com.apple.keylayout.French"
case qwertz = "com.apple.keylayout.ABC-QWERTZ"
case qwertyGerman = "com.apple.keylayout.German"
case azertyFrench = "com.apple.keylayout.French" // 10.9 - 10.12
case qwertzGerman = "com.apple.keylayout.German" // 10.9 - 10.12
case colemak = "com.apple.keylayout.Colemak"
case dvorak = "com.apple.keylayout.Dvorak"
case dvorakQwertyCMD = "com.apple.keylayout.DVORAK-QWERTYCMD"
case dvorakLeft = "com.apple.keylayout.Dvorak-Left"
case dvorakRight = "com.apple.keylayout.Dvorak-Right"
public var mapTable: [UInt16: (String, String)] {
switch self {
case .qwerty, .qwertyUS: return Self.dictQwerty
case .qwerty, .qwertyUS, .qwertyBritish: return Self.dictQwerty
case .azerty, .azertyFrench: return Self.dictAzerty
case .qwertz, .qwertyGerman: return Self.dictQwertz
case .qwertz, .qwertzGerman: return Self.dictQwertz
case .colemak: return Self.dictColemak
case .dvorak: return Self.dictDvorak
case .dvorak, .dvorakQwertyCMD: return Self.dictDvorak
case .dvorakLeft: return Self.dictDvorakLeft
case .dvorakRight: return Self.dictDvorakRight
}

View File

@ -0,0 +1,212 @@
---
# BasedOnStyle: Google
AccessModifierOffset: -1
AlignAfterOpenBracket: Align
AlignArrayOfStructures: None
AlignConsecutiveMacros: None
AlignConsecutiveAssignments: None
AlignConsecutiveBitFields: None
AlignConsecutiveDeclarations: None
AlignEscapedNewlines: Left
AlignOperands: Align
AlignTrailingComments: true
AllowAllArgumentsOnNextLine: true
AllowAllConstructorInitializersOnNextLine: true
AllowAllParametersOfDeclarationOnNextLine: true
AllowShortEnumsOnASingleLine: true
AllowShortBlocksOnASingleLine: Never
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: All
AllowShortLambdasOnASingleLine: All
AllowShortIfStatementsOnASingleLine: WithoutElse
AllowShortLoopsOnASingleLine: true
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: true
AlwaysBreakTemplateDeclarations: Yes
AttributeMacros:
- __capability
BinPackArguments: true
BinPackParameters: true
BraceWrapping:
AfterCaseLabel: false
AfterClass: false
AfterControlStatement: Never
AfterEnum: false
AfterFunction: false
AfterNamespace: false
AfterObjCDeclaration: false
AfterStruct: false
AfterUnion: false
AfterExternBlock: false
BeforeCatch: false
BeforeElse: false
BeforeLambdaBody: false
BeforeWhile: false
IndentBraces: false
SplitEmptyFunction: true
SplitEmptyRecord: true
SplitEmptyNamespace: true
BreakBeforeBinaryOperators: None
BreakBeforeConceptDeclarations: true
BreakBeforeBraces: Attach
BreakBeforeInheritanceComma: false
BreakInheritanceList: BeforeColon
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: false
BreakConstructorInitializers: BeforeColon
BreakAfterJavaFieldAnnotations: false
BreakStringLiterals: true
ColumnLimit: 80
CommentPragmas: "^ IWYU pragma:"
CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: true
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DeriveLineEnding: true
DerivePointerAlignment: true
DisableFormat: false
EmptyLineAfterAccessModifier: Never
EmptyLineBeforeAccessModifier: LogicalBlock
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: true
ForEachMacros:
- foreach
- Q_FOREACH
- BOOST_FOREACH
IfMacros:
- KJ_IF_MAYBE
IncludeBlocks: Regroup
IncludeCategories:
- Regex: '^<ext/.*\.h>'
Priority: 2
SortPriority: 0
CaseSensitive: false
- Regex: '^<.*\.h>'
Priority: 1
SortPriority: 0
CaseSensitive: false
- Regex: "^<.*"
Priority: 2
SortPriority: 0
CaseSensitive: false
- Regex: ".*"
Priority: 3
SortPriority: 0
CaseSensitive: false
IncludeIsMainRegex: "([-_](test|unittest))?$"
IncludeIsMainSourceRegex: ""
IndentAccessModifiers: false
IndentCaseLabels: true
IndentCaseBlocks: false
IndentGotoLabels: true
IndentPPDirectives: None
IndentExternBlock: AfterExternBlock
IndentRequires: false
IndentWidth: 2
IndentWrappedFunctionNames: false
InsertTrailingCommas: None
JavaScriptQuotes: Leave
JavaScriptWrapImports: true
KeepEmptyLinesAtTheStartOfBlocks: false
LambdaBodyIndentation: Signature
MacroBlockBegin: ""
MacroBlockEnd: ""
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBinPackProtocolList: Never
ObjCBlockIndentWidth: 2
ObjCBreakBeforeNestedBlockParam: true
ObjCSpaceAfterProperty: false
ObjCSpaceBeforeProtocolList: true
PenaltyBreakAssignment: 2
PenaltyBreakBeforeFirstCallParameter: 1
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakString: 1000
PenaltyBreakTemplateDeclaration: 10
PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 200
PenaltyIndentedWhitespace: 0
PointerAlignment: Left
PPIndentWidth: -1
RawStringFormats:
- Language: Cpp
Delimiters:
- cc
- CC
- cpp
- Cpp
- CPP
- "c++"
- "C++"
- "cs"
CanonicalDelimiter: ""
BasedOnStyle: google
- Language: TextProto
Delimiters:
- pb
- PB
- proto
- PROTO
EnclosingFunctions:
- EqualsProto
- EquivToProto
- PARSE_PARTIAL_TEXT_PROTO
- PARSE_TEST_PROTO
- PARSE_TEXT_PROTO
- ParseTextOrDie
- ParseTextProtoOrDie
- ParseTestProto
- ParsePartialTestProto
CanonicalDelimiter: pb
BasedOnStyle: google
ReferenceAlignment: Pointer
ReflowComments: true
ShortNamespaceLines: 1
SortIncludes: CaseSensitive
SortJavaStaticImport: Before
SortUsingDeclarations: true
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: true
SpaceBeforeAssignmentOperators: true
SpaceBeforeCaseColon: false
SpaceBeforeCpp11BracedList: false
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceAroundPointerQualifiers: Default
SpaceBeforeRangeBasedForLoopColon: true
SpaceInEmptyBlock: false
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 2
SpacesInAngles: Never
SpacesInConditionalStatement: false
SpacesInContainerLiterals: true
SpacesInCStyleCastParentheses: false
SpacesInLineCommentPrefix:
Minimum: 1
Maximum: -1
SpacesInParentheses: false
SpacesInSquareBrackets: false
SpaceBeforeSquareBrackets: false
BitFieldColonSpacing: Both
Standard: Auto
StatementAttributeLikeMacros:
- Q_EMIT
StatementMacros:
- Q_UNUSED
- QT_REQUIRE_VERSION
TabWidth: 8
UseCRLF: false
UseTab: Never
WhitespaceSensitiveMacros:
- STRINGIZE
- PP_STRINGIZE
- BOOST_PP_STRINGIZE
- NS_SWIFT_NAME
- CF_SWIFT_NAME
---

View File

@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

View File

@ -0,0 +1,31 @@
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "KimoDataReader",
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "KimoDataReader",
targets: ["KimoDataReader"]
),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "ObjcKimoCommunicator",
publicHeadersPath: "include"
),
.target(
name: "KimoDataReader",
dependencies: ["ObjcKimoCommunicator"]
),
.testTarget(
name: "KimoDataReaderTests",
dependencies: ["KimoDataReader"]
),
]
)

View File

@ -0,0 +1,19 @@
# KimoCommunicator
用來與奇摩輸入法進行 NSConnection 通訊的模組,便於直接從奇摩輸入法讀入使用者自訂詞資料庫的資料。
> 免責聲明:
> 與奇摩輸入法有關的原始碼是由 Yahoo 奇摩以 `SPDX Identifier: BSD-3-Clause` 釋出的,
> 但敝模組只是藉由其 Protocol API 與該當程式進行跨執行緒通訊,所以屬於合理使用範圍。
```
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
```
$ EOF.

View File

@ -0,0 +1,25 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import Foundation
import ObjcKimoCommunicator
public class KimoCommunicator: ObjcKimoCommunicator {
public static let shared: KimoCommunicator = .init()
public func prepareData(handler: @escaping (_ key: String, _ value: String) -> Void) {
guard KimoCommunicator.shared.establishConnection() else { return }
assert(KimoCommunicator.shared.hasValidConnection())
let loopAmount = KimoCommunicator.shared.userPhraseDBTotalAmountOfRows()
for i in 0 ..< loopAmount {
let fetched = KimoCommunicator.shared.userPhraseDBDictionary(atRow: i)
guard let key = fetched["BPMF"], let text = fetched["Text"] else { continue }
handler(key, text)
}
}
}

View File

@ -0,0 +1,86 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
//
// Yahoo `SPDX Identifier: BSD-3-Clause`
// Protocol API 使
#import "KimoCommunicator.h"
#import <AppKit/AppKit.h>
#import <Foundation/Foundation.h>
#define kYahooKimoDataObjectConnectionName @"YahooKeyKeyService"
@implementation ObjcKimoCommunicator {
id _xpcConnection;
}
///
- (void)dealloc {
[self disconnect];
}
///
- (void)disconnect {
_xpcConnection = nil;
}
///
- (bool)establishConnection {
// 2012 NSXPCConnection
// NSXPCConnection 使 NSXPCConnection
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
_xpcConnection = [NSConnection rootProxyForConnectionWithRegisteredName:
kYahooKimoDataObjectConnectionName
host:nil];
#pragma GCC diagnostic pop
BOOL result = false;
if (_xpcConnection) {
result = true;
}
if (result) {
[_xpcConnection setProtocolForProxy:@protocol(KimoUserDataReaderService)];
NSLog(@"vChewingDebug: Connection successful. Available data amount: %d.\n",
[_xpcConnection userPhraseDBNumberOfRow]);
}
return result;
}
///
- (bool)hasValidConnection {
BOOL result = false;
if (_xpcConnection) result = true;
return result;
}
- (BOOL)userPhraseDBCanProvideService {
return [self hasValidConnection]
? [_xpcConnection userPhraseDBCanProvideService]
: NO;
}
- (int)userPhraseDBTotalAmountOfRows {
return [self hasValidConnection] ? [_xpcConnection userPhraseDBNumberOfRow]
: 0;
}
- (NSDictionary<NSString*, NSString*> *)userPhraseDBDictionaryAtRow:(int)row {
return [self hasValidConnection]
? [_xpcConnection userPhraseDBDictionaryAtRow:row]
: [NSDictionary alloc];
}
- (bool)exportUserPhraseDBToFile:(NSString *)path {
return [self hasValidConnection]
? [_xpcConnection exportUserPhraseDBToFile:path]
: NO;
}
@end

View File

@ -0,0 +1,46 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
// 免責聲明:
// 與奇摩輸入法有關的原始碼是由 Yahoo 奇摩以 `SPDX Identifier: BSD-3-Clause` 釋出的,
// 但敝模組只是藉由其 Protocol API 與該當程式進行跨執行緒通訊,所以屬於合理使用範圍。
#import <AppKit/AppKit.h>
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@protocol KimoUserDataReaderService
- (BOOL)userPhraseDBCanProvideService;
- (int)userPhraseDBNumberOfRow;
- (NSDictionary *)userPhraseDBDictionaryAtRow:(int)row;
- (bool)exportUserPhraseDBToFile:(NSString *)path;
@end
/// 不要理會 Xcode 對 NSDistantObject 的過期狗吠。
/// 奇摩輸入法是用 NSConnection 寫的,
/// 換用 NSXPCConnection 只會製造更多的問題。
@interface ObjcKimoCommunicator : NSObject
/// 嘗試連線。
- (bool)establishConnection;
/// 偵測連線是否有效。
- (bool)hasValidConnection;
/// 斷開連線。
- (void)disconnect;
// Conforming KimoUserDataReaderService protocol.
- (BOOL)userPhraseDBCanProvideService;
- (int)userPhraseDBTotalAmountOfRows;
- (NSDictionary<NSString*, NSString*> *)userPhraseDBDictionaryAtRow:(int)row;
- (bool)exportUserPhraseDBToFile:(NSString *)path;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,10 @@
@testable import ObjcKimoCommunicator
import XCTest
final class KimoDataReaderTests: XCTestCase {
//
func testExample() throws {
let shared = ObjcKimoCommunicator()
print(shared.establishConnection())
}
}

View File

@ -0,0 +1,6 @@
find . -regex '.*\.\(hh\)' -exec clang-format -style=file -i {} \;
find . -regex '.*\.\(cc\)' -exec clang-format -style=file -i {} \;
find . -regex '.*\.\(mm\)' -exec clang-format -style=file -i {} \;
find . -regex '.*\.\(h\)' -exec clang-format -style=file -i {} \;
find . -regex '.*\.\(c\)' -exec clang-format -style=file -i {} \;
find . -regex '.*\.\(m\)' -exec clang-format -style=file -i {} \;

View File

@ -15,8 +15,7 @@ let package = Package(
dependencies: [
.package(path: "../RMJay_LineReader"),
.package(path: "../vChewing_Megrez"),
.package(path: "../vChewing_PinyinPhonaConverter"),
.package(path: "../vChewing_Shared"),
.package(path: "../vChewing_SwiftExtension"),
],
targets: [
.target(
@ -24,12 +23,7 @@ let package = Package(
dependencies: [
.product(name: "LineReader", package: "RMJay_LineReader"),
.product(name: "Megrez", package: "vChewing_Megrez"),
.product(name: "Shared", package: "vChewing_Shared"),
.product(name: "PinyinPhonaConverter", package: "vChewing_PinyinPhonaConverter"),
],
resources: [
.process("Resources/sequenceDataFromEtenDOS-chs.json"),
.process("Resources/sequenceDataFromEtenDOS-cht.json"),
.product(name: "SwiftExtension", package: "vChewing_SwiftExtension"),
]
),
.testTarget(

View File

@ -1,17 +1,17 @@
# LangModelAssembly
威注音輸入法的語言模組總成套裝
威注音輸入法的語言模組總成套裝,以 LMAssembly 命名空間承載下述唯二對外物件:
- vChewingLM總命名空間也承載一些在套裝內共用的工具函式。
- LMConsolidator自動格式整理模組。
- LMInstantiator語言模組副本化模組。另有其日期時間擴充模組可用對 CIN 磁帶模式無效)。
- LMInstantiator語言模組副本化模組亦集成一些自身功能擴展。
LMAssembly 總命名空間也承載一些在套裝內共用的工具函式。
以下是子模組:
- lmCassette專門用來處理 CIN 磁帶檔案的模組,命名為「遠野」引擎。
- LMAssociates關聯詞語模組。
- lmCassette專門用來處理 CIN 磁帶檔案的模組,命名為「遠野」引擎。
- LMCoreEX可以直接讀取 TXT 格式的帶有權重資料的語彙檔案的模組。
- LMCoreJSON專門用來讀取原廠 JSON 檔案的模組。
- lmPlainBopomofo專門用來讀取使用者自訂ㄅ半候選字順序覆蓋定義檔案plist的模組。
- lmReplacements專門用來讀取使用者語彙置換模式的辭典資料的模組。
- lmUserOverride半衰記憶模組。

View File

@ -11,29 +11,31 @@ import Foundation
/// InputToken.parse Token
/// Token .translated()
public enum InputToken {
case timeZone(shortened: Bool)
case timeNow(shortened: Bool)
case date(dayDelta: Int = 0, yearDelta: Int = 0, shortened: Bool = true, luna: Bool = false)
case week(dayDelta: Int = 0, shortened: Bool = true)
case year(yearDelta: Int = 0)
case yearGanzhi(yearDelta: Int = 0)
case yearZodiac(yearDelta: Int = 0)
extension LMAssembly {
enum InputToken {
case timeZone(shortened: Bool)
case timeNow(shortened: Bool)
case date(dayDelta: Int = 0, yearDelta: Int = 0, shortened: Bool = true, luna: Bool = false)
case week(dayDelta: Int = 0, shortened: Bool = true)
case year(yearDelta: Int = 0)
case yearGanzhi(yearDelta: Int = 0)
case yearZodiac(yearDelta: Int = 0)
}
}
// MARK: - 使 API
public extension String {
func parseAsInputToken(isCHS: Bool) -> [String] {
InputToken.parse(from: self).map { $0.translated(isCHS: isCHS) }.flatMap { $0 }.deduplicated
LMAssembly.InputToken.parse(from: self).map { $0.translated(isCHS: isCHS) }.flatMap { $0 }.deduplicated
}
}
// MARK: - Parser parsing raw token value to construct token.
public extension InputToken {
static func parse(from rawToken: String) -> [InputToken] {
var result: [InputToken] = []
extension LMAssembly.InputToken {
static func parse(from rawToken: String) -> [LMAssembly.InputToken] {
var result: [LMAssembly.InputToken] = []
guard rawToken.prefix(6) == "MACRO@" else { return result }
var mapParams: [String: Int] = [:]
let tokenComponents = rawToken.dropFirst(6).split(separator: "_").map { param in
@ -69,7 +71,7 @@ public extension InputToken {
// MARK: - Parser parsing token itself.
public extension InputToken {
extension LMAssembly.InputToken {
func translated(isCHS: Bool) -> [String] {
let locale = Locale(identifier: isCHS ? "zh-Hans" : "zh-Hant-TW")
let formatter = DateFormatter()

View File

@ -8,9 +8,8 @@
import Foundation
import LineReader
import Shared
public extension vChewingLM {
public extension LMAssembly {
enum LMConsolidator {
public static let kPragmaHeader = "# 𝙵𝙾𝚁𝙼𝙰𝚃 𝚘𝚛𝚐.𝚊𝚝𝚎𝚕𝚒𝚎𝚛𝙸𝚗𝚖𝚞.𝚟𝚌𝚑𝚎𝚠𝚒𝚗𝚐.𝚞𝚜𝚎𝚛𝙻𝚊𝚗𝚐𝚞𝚊𝚐𝚎𝙼𝚘𝚍𝚎𝚕𝙳𝚊𝚝𝚊.𝚏𝚘𝚛𝚖𝚊𝚝𝚝𝚎𝚍"
@ -26,19 +25,19 @@ public extension vChewingLM {
let lineReader = try LineReader(file: fileHandle)
for strLine in lineReader { // i=0
if strLine != kPragmaHeader {
vCLog("Header Mismatch, Starting In-Place Consolidation.")
vCLMLog("Header Mismatch, Starting In-Place Consolidation.")
return false
} else {
vCLog("Header Verification Succeeded: \(strLine).")
vCLMLog("Header Verification Succeeded: \(strLine).")
return true
}
}
} catch {
vCLog("Header Verification Failed: File Access Error.")
vCLMLog("Header Verification Failed: File Access Error.")
return false
}
}
vCLog("Header Verification Failed: File Missing.")
vCLMLog("Header Verification Failed: File Missing.")
return false
}
@ -51,12 +50,12 @@ public extension vChewingLM {
let dict = try FileManager.default.attributesOfItem(atPath: path)
if let value = dict[FileAttributeKey.size] as? UInt64 { fileSize = value }
} catch {
vCLog("EOF Fix Failed: File Missing at \(path).")
vCLMLog("EOF Fix Failed: File Missing at \(path).")
return false
}
guard let fileSize = fileSize else { return false }
guard let writeFile = FileHandle(forUpdatingAtPath: path) else {
vCLog("EOF Fix Failed: File Not Writable at \(path).")
vCLMLog("EOF Fix Failed: File Not Writable at \(path).")
return false
}
defer { writeFile.closeFile() }
@ -64,11 +63,11 @@ public extension vChewingLM {
/// consolidate()
writeFile.seek(toFileOffset: fileSize - 1)
if writeFile.readDataToEndOfFile().first != 0x0A {
vCLog("EOF Missing Confirmed, Start Fixing.")
vCLMLog("EOF Missing Confirmed, Start Fixing.")
var newData = Data()
newData.append(0x0A)
writeFile.write(newData)
vCLog("EOF Successfully Assured.")
vCLMLog("EOF Successfully Assured.")
}
return false
}
@ -142,14 +141,29 @@ public extension vChewingLM {
// Write consolidated file contents.
try strProcessed.write(to: urlPath, atomically: false, encoding: .utf8)
} catch {
vCLog("Consolidation Failed w/ File: \(path), error: \(error)")
vCLMLog("Consolidation Failed w/ File: \(path), error: \(error)")
return false
}
vCLog("Either Consolidation Successful Or No-Need-To-Consolidate.")
vCLMLog("Either Consolidation Successful Or No-Need-To-Consolidate.")
return true
}
vCLog("Consolidation Failed: File Missing at \(path).")
vCLMLog("Consolidation Failed: File Missing at \(path).")
return false
}
}
}
private extension String {
mutating func regReplace(pattern: String, replaceWith: String = "") {
// Ref: https://stackoverflow.com/a/40993403/4162914 && https://stackoverflow.com/a/71291137/4162914
do {
let regex = try NSRegularExpression(
pattern: pattern, options: [.caseInsensitive, .anchorsMatchLines]
)
let range = NSRange(startIndex..., in: self)
self = regex.stringByReplacingMatches(
in: self, options: [], range: range, withTemplate: replaceWith
)
} catch { return }
}
}

View File

@ -8,10 +8,8 @@
import Foundation
import Megrez
import Shared
import SQLite3
public extension vChewingLM {
public extension LMAssembly {
/// LMInstantiatorLMI
/// LangModelProtocol 使
///
@ -39,45 +37,40 @@ public extension vChewingLM {
public var isCNSEnabled = false
public var isSymbolEnabled = false
public var isSCPCEnabled = false
public var filterNonCNSReadings = false
public var deltaOfCalendarYears: Int = -2000
}
public static var asyncLoadingUserData: Bool = true
// SQLite
static var ptrSQL: OpaquePointer?
// SQLite
public private(set) static var isSQLDBConnected: Bool = false
public internal(set) static var isSQLDBConnected: Bool = false
//
public let isCHS: Bool
//
public var config = Config()
public private(set) var config = Config()
// package
public init(isCHS: Bool = false) {
public init(
isCHS: Bool = false,
uomDataURL: URL? = nil
) {
self.isCHS = isCHS
lmUserOverride = .init(dataURL: uomDataURL)
}
public func setOptions(handler: (inout Config) -> Void) {
@discardableResult public func setOptions(handler: (inout Config) -> Void) -> LMInstantiator {
handler(&config)
return self
}
@discardableResult public static func connectSQLDB(dbPath: String, dropPreviousConnection: Bool = true) -> Bool {
if dropPreviousConnection { disconnectSQLDB() }
vCLog("Establishing SQLite connection to: \(dbPath)")
guard sqlite3_open(dbPath, &Self.ptrSQL) == SQLITE_OK else { return false }
guard "PRAGMA journal_mode = OFF;".runAsSQLExec(dbPointer: &ptrSQL) else { return false }
isSQLDBConnected = true
return true
}
public static func disconnectSQLDB() {
if Self.ptrSQL != nil {
sqlite3_close_v2(Self.ptrSQL)
Self.ptrSQL = nil
}
isSQLDBConnected = false
public static func setCassetCandidateKeyValidator(_ validator: @escaping (String) -> Bool) {
Self.lmCassette.candidateKeysValidator = validator
}
///
@ -92,6 +85,7 @@ public extension vChewingLM {
// currentCassetteMetadata
static var lmCassette = LMCassette()
static var lmPlainBopomofo = LMPlainBopomofo()
// 使
// 使使
@ -106,30 +100,46 @@ public extension vChewingLM {
)
var lmReplacements = LMReplacements()
var lmAssociates = LMAssociates()
var lmPlainBopomofo = LMPlainBopomofo()
//
var lmUserOverride: LMUserOverride
// MARK: -
public func resetFactoryJSONModels() {}
public func loadUserPhrasesData(path: String, filterPath: String?) {
DispatchQueue.main.async {
func loadMain() {
if FileManager.default.isReadableFile(atPath: path) {
self.lmUserPhrases.clear()
self.lmUserPhrases.open(path)
vCLog("lmUserPhrases: \(self.lmUserPhrases.count) entries of data loaded from: \(path)")
lmUserPhrases.clear()
lmUserPhrases.open(path)
vCLMLog("lmUserPhrases: \(lmUserPhrases.count) entries of data loaded from: \(path)")
} else {
vCLog("lmUserPhrases: File access failure: \(path)")
vCLMLog("lmUserPhrases: File access failure: \(path)")
}
}
if !Self.asyncLoadingUserData {
loadMain()
} else {
DispatchQueue.main.async {
loadMain()
}
}
guard let filterPath = filterPath else { return }
DispatchQueue.main.async {
func loadFilter() {
if FileManager.default.isReadableFile(atPath: filterPath) {
self.lmFiltered.clear()
self.lmFiltered.open(filterPath)
vCLog("lmFiltered: \(self.lmFiltered.count) entries of data loaded from: \(path)")
lmFiltered.clear()
lmFiltered.open(filterPath)
vCLMLog("lmFiltered: \(lmFiltered.count) entries of data loaded from: \(path)")
} else {
vCLog("lmFiltered: File access failure: \(path)")
vCLMLog("lmFiltered: File access failure: \(path)")
}
}
if !Self.asyncLoadingUserData {
loadFilter()
} else {
DispatchQueue.main.async {
loadFilter()
}
}
}
@ -139,74 +149,85 @@ public extension vChewingLM {
if FileManager.default.isReadableFile(atPath: path) {
lmFiltered.clear()
lmFiltered.open(path)
vCLog("lmFiltered: \(lmFiltered.count) entries of data loaded from: \(path)")
vCLMLog("lmFiltered: \(lmFiltered.count) entries of data loaded from: \(path)")
} else {
vCLog("lmFiltered: File access failure: \(path)")
vCLMLog("lmFiltered: File access failure: \(path)")
}
}
public func loadUserSymbolData(path: String) {
DispatchQueue.main.async {
func load() {
if FileManager.default.isReadableFile(atPath: path) {
self.lmUserSymbols.clear()
self.lmUserSymbols.open(path)
vCLog("lmUserSymbol: \(self.lmUserSymbols.count) entries of data loaded from: \(path)")
lmUserSymbols.clear()
lmUserSymbols.open(path)
vCLMLog("lmUserSymbol: \(lmUserSymbols.count) entries of data loaded from: \(path)")
} else {
vCLog("lmUserSymbol: File access failure: \(path)")
vCLMLog("lmUserSymbol: File access failure: \(path)")
}
}
if !Self.asyncLoadingUserData {
load()
} else {
DispatchQueue.main.async {
load()
}
}
}
public func loadUserAssociatesData(path: String) {
DispatchQueue.main.async {
func load() {
if FileManager.default.isReadableFile(atPath: path) {
self.lmAssociates.clear()
self.lmAssociates.open(path)
vCLog("lmAssociates: \(self.lmAssociates.count) entries of data loaded from: \(path)")
lmAssociates.clear()
lmAssociates.open(path)
vCLMLog("lmAssociates: \(lmAssociates.count) entries of data loaded from: \(path)")
} else {
vCLog("lmAssociates: File access failure: \(path)")
vCLMLog("lmAssociates: File access failure: \(path)")
}
}
if !Self.asyncLoadingUserData {
load()
} else {
DispatchQueue.main.async {
load()
}
}
}
public func loadReplacementsData(path: String) {
DispatchQueue.main.async {
func load() {
if FileManager.default.isReadableFile(atPath: path) {
self.lmReplacements.clear()
self.lmReplacements.open(path)
vCLog("lmReplacements: \(self.lmReplacements.count) entries of data loaded from: \(path)")
lmReplacements.clear()
lmReplacements.open(path)
vCLMLog("lmReplacements: \(lmReplacements.count) entries of data loaded from: \(path)")
} else {
vCLog("lmReplacements: File access failure: \(path)")
vCLMLog("lmReplacements: File access failure: \(path)")
}
}
}
public func loadSCPCSequencesData() {
let fileName = !isCHS ? "sequenceDataFromEtenDOS-cht" : "sequenceDataFromEtenDOS-chs"
guard let path = Bundle.module.path(forResource: fileName, ofType: "json") else {
vCLog("lmPlainBopomofo: File name access failure: \(fileName)")
return
}
DispatchQueue.main.async {
if FileManager.default.isReadableFile(atPath: path) {
self.lmPlainBopomofo.clear()
self.lmPlainBopomofo.open(path)
vCLog("lmPlainBopomofo: \(self.lmPlainBopomofo.count) entries of data loaded from: \(path)")
} else {
vCLog("lmPlainBopomofo: File access failure: \(path)")
if !Self.asyncLoadingUserData {
load()
} else {
DispatchQueue.main.async {
load()
}
}
}
public var isCassetteDataLoaded: Bool { Self.lmCassette.isLoaded }
public static func loadCassetteData(path: String) {
DispatchQueue.main.async {
func load() {
if FileManager.default.isReadableFile(atPath: path) {
Self.lmCassette.clear()
Self.lmCassette.open(path)
vCLog("lmCassette: \(Self.lmCassette.count) entries of data loaded from: \(path)")
vCLMLog("lmCassette: \(Self.lmCassette.count) entries of data loaded from: \(path)")
} else {
vCLog("lmCassette: File access failure: \(path)")
vCLMLog("lmCassette: File access failure: \(path)")
}
}
if !Self.asyncLoadingUserData {
load()
} else {
DispatchQueue.main.async {
load()
}
}
}
@ -332,20 +353,27 @@ public extension vChewingLM {
// 使
if config.isSCPCEnabled {
rawAllUnigrams += lmPlainBopomofo.valuesFor(key: keyChain).map { Megrez.Unigram(value: $0, score: 0) }
rawAllUnigrams += Self.lmPlainBopomofo.valuesFor(key: keyChain, isCHS: isCHS).map {
Megrez.Unigram(value: $0, score: 0)
}
}
// reversed 使
//
// rawUserUnigrams
rawAllUnigrams += lmUserPhrases.unigramsFor(key: keyChain).reversed()
if !config.isCassetteEnabled || config.isCassetteEnabled && keyChain.map(\.description)[0] == "_" {
// NumPad
rawAllUnigrams += supplyNumPadUnigrams(key: keyChain)
// LMMisc LMCore score (-10.0, 0.0)
rawAllUnigrams += factoryUnigramsFor(key: keyChain, column: .theDataCHEW)
rawAllUnigrams += factoryCoreUnigramsFor(key: keyChain)
//
var coreUnigramsResult: [Megrez.Unigram] = factoryCoreUnigramsFor(key: keyChain)
// CNS11643
if config.filterNonCNSReadings, !isCHS {
coreUnigramsResult.removeAll { thisUnigram in
!checkCNSConformation(for: thisUnigram, keyArray: keyArray)
}
}
//
rawAllUnigrams += coreUnigramsResult
if config.isCNSEnabled {
rawAllUnigrams += factoryUnigramsFor(key: keyChain, column: .theDataCNS)
}
@ -358,6 +386,21 @@ public extension vChewingLM {
}
}
// reversed 使
//
// rawUserUnigrams
var userPhraseUnigrams = Array(lmUserPhrases.unigramsFor(key: keyChain).reversed())
if keyArray.count == 1, let topScore = rawAllUnigrams.map(\.score).max() {
// 使
userPhraseUnigrams = userPhraseUnigrams.map { currentUnigram in
Megrez.Unigram(
value: currentUnigram.value,
score: Swift.min(topScore + 0.000_114_514, currentUnigram.score)
)
}
}
rawAllUnigrams = userPhraseUnigrams + rawAllUnigrams
// InputToken
rawAllUnigrams = rawAllUnigrams.map { unigram in
let convertedValues = unigram.value.parseAsInputToken(isCHS: isCHS)

View File

@ -8,9 +8,9 @@
import Foundation
import Megrez
import Shared
import SwiftExtension
public extension vChewingLM.LMInstantiator {
public extension LMAssembly.LMInstantiator {
///
var cassetteWildcardKey: String { Self.lmCassette.wildcardKey }
///

View File

@ -11,7 +11,7 @@ import Megrez
// MARK: - 便
extension vChewingLM.LMInstantiator {
extension LMAssembly.LMInstantiator {
func queryDateTimeUnigrams(with key: String = "") -> [Megrez.Unigram] {
guard let tokenTrigger = TokenTrigger(rawValue: key) else { return [] }
var results = [Megrez.Unigram]()

View File

@ -9,7 +9,7 @@
import Foundation
import Megrez
public extension vChewingLM.LMInstantiator {
public extension LMAssembly.LMInstantiator {
func supplyNumPadUnigrams(key: String) -> [Megrez.Unigram] {
guard let status = config.numPadFWHWStatus else { return [] }
let initials = "_NumPad_"

View File

@ -8,7 +8,6 @@
import Foundation
import Megrez
import Shared
import SQLite3
/* ==============
@ -31,30 +30,49 @@ import SQLite3
) WITHOUT ROWID;
*/
enum CoreColumn: Int32 {
case theDataCHS = 1 //
case theDataCHT = 2 //
case theDataCNS = 3 //
case theDataMISC = 4 //
case theDataSYMB = 5 //
case theDataCHEW = 6 //
extension LMAssembly.LMInstantiator {
enum CoreColumn: Int32 {
case theDataCHS = 1 //
case theDataCHT = 2 //
case theDataCNS = 3 //
case theDataMISC = 4 //
case theDataSYMB = 5 //
case theDataCHEW = 6 //
var name: String { String(describing: self) }
var name: String { String(describing: self) }
var id: Int32 { rawValue }
var id: Int32 { rawValue }
var defaultScore: Double {
switch self {
case .theDataCHEW: return -1
case .theDataCNS: return -11
case .theDataSYMB: return -13
case .theDataMISC: return -10
default: return -9.9
var defaultScore: Double {
switch self {
case .theDataCHEW: return -1
case .theDataCNS: return -11
case .theDataSYMB: return -13
case .theDataMISC: return -10
default: return -9.9
}
}
}
}
extension vChewingLM.LMInstantiator {
extension LMAssembly.LMInstantiator {
@discardableResult public static func connectSQLDB(dbPath: String, dropPreviousConnection: Bool = true) -> Bool {
if dropPreviousConnection { disconnectSQLDB() }
vCLMLog("Establishing SQLite connection to: \(dbPath)")
guard sqlite3_open(dbPath, &Self.ptrSQL) == SQLITE_OK else { return false }
guard "PRAGMA journal_mode = OFF;".runAsSQLExec(dbPointer: &ptrSQL) else { return false }
isSQLDBConnected = true
return true
}
public static func disconnectSQLDB() {
if Self.ptrSQL != nil {
sqlite3_close_v2(Self.ptrSQL)
Self.ptrSQL = nil
}
isSQLDBConnected = false
}
fileprivate static func querySQL(strStmt sqlQuery: String, coreColumn column: CoreColumn, handler: (String) -> Void) {
guard Self.ptrSQL != nil else { return }
performStatementSansResult { ptrStatement in
@ -123,9 +141,10 @@ extension vChewingLM.LMInstantiator {
}
/// UTF8
/// - Remark: 使
/// - parameters:
/// - key:
func factoryCoreUnigramsFor(key: String) -> [Megrez.Unigram] {
public func factoryCoreUnigramsFor(key: String) -> [Megrez.Unigram] {
// ASCII SQLite
factoryUnigramsFor(key: key, column: isCHS ? .theDataCHS : .theDataCHT)
}
@ -134,7 +153,9 @@ extension vChewingLM.LMInstantiator {
/// - parameters:
/// - key:
/// - column:
func factoryUnigramsFor(key: String, column: CoreColumn) -> [Megrez.Unigram] {
func factoryUnigramsFor(
key: String, column: LMAssembly.LMInstantiator.CoreColumn
) -> [Megrez.Unigram] {
if key == "_punctuation_list" { return [] }
var grams: [Megrez.Unigram] = []
var gramsHW: [Megrez.Unigram] = []
@ -142,8 +163,10 @@ extension vChewingLM.LMInstantiator {
let encryptedKey = Self.cnvPhonabetToASCII(key.replacingOccurrences(of: "'", with: "''"))
let sqlQuery = "SELECT * FROM DATA_MAIN WHERE theKey='\(encryptedKey)';"
Self.querySQL(strStmt: sqlQuery, coreColumn: column) { currentResult in
let arrRangeRecords = currentResult.split(separator: "\t")
for strNetaSet in arrRangeRecords {
var i: Double = 0
var previousScore: Double?
currentResult.split(separator: "\t").forEach { strNetaSet in
// stable sort
let neta = Array(strNetaSet.trimmingCharacters(in: .newlines).split(separator: " ").reversed())
let theValue: String = .init(neta[0])
var theScore = column.defaultScore
@ -153,8 +176,15 @@ extension vChewingLM.LMInstantiator {
if theScore > 0 {
theScore *= -1 //
}
if previousScore == theScore {
theScore -= i * 0.000_001
i += 1
} else {
previousScore = theScore
i = 0
}
grams.append(Megrez.Unigram(value: theValue, score: theScore))
if !key.contains("_punctuation") { continue }
if !key.contains("_punctuation") { return }
let halfValue = theValue.applyingTransformFW2HW(reverse: false)
if halfValue != theValue {
gramsHW.append(Megrez.Unigram(value: halfValue, score: theScore))
@ -165,6 +195,24 @@ extension vChewingLM.LMInstantiator {
return grams
}
/// CNS UTF8
/// CNS
/// - parameters:
/// - key:
/// - column:
private func factoryCNSFilterThreadFor(key: String) -> String? {
let column = CoreColumn.theDataCNS
if key == "_punctuation_list" { return nil }
var results: [String] = []
// ASCII SQLite
let encryptedKey = Self.cnvPhonabetToASCII(key.replacingOccurrences(of: "'", with: "''"))
let sqlQuery = "SELECT * FROM DATA_MAIN WHERE theKey='\(encryptedKey)';"
Self.querySQL(strStmt: sqlQuery, coreColumn: column) { currentResult in
results.append(currentResult)
}
return results.joined(separator: "\t")
}
/// UTF8
/// - remark:
/// - parameters:
@ -177,9 +225,22 @@ extension vChewingLM.LMInstantiator {
let sqlQuery = "SELECT * FROM DATA_MAIN WHERE theKey='\(encryptedKey)' AND \(column.name) IS NOT NULL"
return Self.hasSQLResult(strStmt: sqlQuery)
}
/// Unigram CNS11643
/// 使
func checkCNSConformation(for unigram: Megrez.Unigram, keyArray: [String]) -> Bool {
guard unigram.value.count == keyArray.count else { return true }
let chars = unigram.value.map(\.description)
for (i, key) in keyArray.enumerated() {
guard !key.hasPrefix("_") else { continue }
guard let matchedCNSResult = factoryCNSFilterThreadFor(key: key) else { continue }
guard matchedCNSResult.contains(chars[i]) else { return false }
}
return true
}
}
private extension vChewingLM.LMInstantiator {
private extension LMAssembly.LMInstantiator {
///
///
/// 使 json
@ -227,7 +288,7 @@ private extension vChewingLM.LMInstantiator {
]
}
public extension vChewingLM.LMInstantiator {
public extension LMAssembly.LMInstantiator {
@discardableResult static func connectToTestSQLDB() -> Bool {
Self.connectSQLDB(dbPath: #":memory:"#) && sqlTestCoreLMData.runAsSQLExec(dbPointer: &ptrSQL)
}

View File

@ -0,0 +1,60 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import Foundation
import Megrez
public extension LMAssembly.LMInstantiator {
func performUOMObservation(
walkedBefore: [Megrez.Node],
walkedAfter: [Megrez.Node],
cursor: Int,
timestamp: Double,
saveCallback: (() -> Void)? = nil
) {
lmUserOverride.performObservation(
walkedBefore: walkedBefore,
walkedAfter: walkedAfter,
cursor: cursor,
timestamp: timestamp,
saveCallback: saveCallback
)
}
func fetchUOMSuggestion(
currentWalk: [Megrez.Node],
cursor: Int,
timestamp: Double
) -> LMAssembly.OverrideSuggestion {
lmUserOverride.fetchSuggestion(
currentWalk: currentWalk,
cursor: cursor,
timestamp: timestamp
)
}
func loadUOMData(fromURL fileURL: URL? = nil) {
lmUserOverride.loadData(fromURL: fileURL)
}
func saveUOMData(toURL fileURL: URL? = nil) {
lmUserOverride.saveData(toURL: fileURL)
}
func clearUOMData(withURL fileURL: URL? = nil) {
lmUserOverride.clearData(withURL: fileURL)
}
func bleachSpecifiedUOMSuggestions(targets: [String], saveCallback: (() -> Void)? = nil) {
lmUserOverride.bleachSpecifiedSuggestions(targets: targets, saveCallback: saveCallback)
}
func bleachUOMUnigrams(saveCallback: (() -> Void)? = nil) {
lmUserOverride.bleachUnigrams(saveCallback: saveCallback)
}
}

View File

@ -0,0 +1,33 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import Foundation
public extension LMAssembly {
struct UserDictionarySummarized: Codable {
let isCHS: Bool
let userPhrases: [String: [String]]
let filter: [String: [String]]
let userSymbols: [String: [String]]
let replacements: [String: String]
let associates: [String: [String]]
}
}
public extension LMAssembly.LMInstantiator {
func summarize(all: Bool) -> LMAssembly.UserDictionarySummarized {
LMAssembly.UserDictionarySummarized(
isCHS: isCHS,
userPhrases: lmUserPhrases.dictRepresented,
filter: lmFiltered.dictRepresented,
userSymbols: lmUserSymbols.dictRepresented,
replacements: lmReplacements.dictRepresented,
associates: all ? lmAssociates.dictRepresented : [:]
)
}
}

View File

@ -0,0 +1,123 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import Foundation
// 使 LMAssembly Tekkon
private typealias LengthSortedDictionary = [Int: [String: String]]
private let mapHanyuPinyinToPhonabets: LengthSortedDictionary = {
let parsed = try? JSONDecoder().decode(LengthSortedDictionary.self, from: jsnHanyuPinyinToMPS.data(using: .utf8) ?? Data([]))
return parsed ?? [:]
}()
extension String {
mutating func convertToPhonabets(newToneOne: String = "") {
if isEmpty || contains("_") || !isNotPureAlphanumerical { return }
let lengths = mapHanyuPinyinToPhonabets.keys.sorted().reversed()
lengths.forEach { length in
mapHanyuPinyinToPhonabets[length]?.forEach { key, value in
self = replacingOccurrences(of: key, with: value)
}
}
self = replacingOccurrences(of: " ", with: newToneOne)
}
}
///
private extension String {
var isNotPureAlphanumerical: Bool {
let regex = ".*[^A-Za-z0-9].*"
let testString = NSPredicate(format: "SELF MATCHES %@", regex)
return testString.evaluate(with: self)
}
}
private let jsnHanyuPinyinToMPS = #"""
{
"1":{"1":" ","2":"ˊ","3":"ˇ","4":"ˋ","5":"˙","a":"","e":"","o":"","q":""},
"2":{"ai":"","an":"","ao":"","ba":"ㄅㄚ","bi":"ㄅㄧ","bo":"ㄅㄛ","bu":"ㄅㄨ",
"ca":"ㄘㄚ","ce":"ㄘㄜ","ci":"","cu":"ㄘㄨ","da":"ㄉㄚ","de":"ㄉㄜ","di":"ㄉㄧ",
"du":"ㄉㄨ","eh":"","ei":"","en":"","er":"","fa":"ㄈㄚ","fo":"ㄈㄛ",
"fu":"ㄈㄨ","ga":"ㄍㄚ","ge":"ㄍㄜ","gi":"ㄍㄧ","gu":"ㄍㄨ","ha":"ㄏㄚ","he":"ㄏㄜ",
"hu":"ㄏㄨ","ji":"ㄐㄧ","ju":"ㄐㄩ","ka":"ㄎㄚ","ke":"ㄎㄜ","ku":"ㄎㄨ","la":"ㄌㄚ",
"le":"ㄌㄜ","li":"ㄌㄧ","lo":"ㄌㄛ","lu":"ㄌㄨ","lv":"ㄌㄩ","ma":"ㄇㄚ","me":"ㄇㄜ",
"mi":"ㄇㄧ","mo":"ㄇㄛ","mu":"ㄇㄨ","na":"ㄋㄚ","ne":"ㄋㄜ","ni":"ㄋㄧ","nu":"ㄋㄨ",
"nv":"ㄋㄩ","ou":"","pa":"ㄆㄚ","pi":"ㄆㄧ","po":"ㄆㄛ","pu":"ㄆㄨ","qi":"ㄑㄧ",
"qu":"ㄑㄩ","re":"ㄖㄜ","ri":"","ru":"ㄖㄨ","sa":"ㄙㄚ","se":"ㄙㄜ","si":"",
"su":"ㄙㄨ","ta":"ㄊㄚ","te":"ㄊㄜ","ti":"ㄊㄧ","tu":"ㄊㄨ","wa":"ㄨㄚ","wo":"ㄨㄛ",
"wu":"","xi":"ㄒㄧ","xu":"ㄒㄩ","ya":"ㄧㄚ","ye":"ㄧㄝ","yi":"","yo":"ㄧㄛ",
"yu":"","za":"ㄗㄚ","ze":"ㄗㄜ","zi":"","zu":"ㄗㄨ"},
"3":{"ang":"","bai":"ㄅㄞ","ban":"ㄅㄢ","bao":"ㄅㄠ","bei":"ㄅㄟ","ben":"ㄅㄣ",
"bie":"ㄅㄧㄝ","bin":"ㄅㄧㄣ","cai":"ㄘㄞ","can":"ㄘㄢ","cao":"ㄘㄠ","cei":"ㄘㄟ",
"cen":"ㄘㄣ","cha":"ㄔㄚ","che":"ㄔㄜ","chi":"","chu":"ㄔㄨ","cou":"ㄘㄡ",
"cui":"ㄘㄨㄟ","cun":"ㄘㄨㄣ","cuo":"ㄘㄨㄛ","dai":"ㄉㄞ","dan":"ㄉㄢ","dao":"ㄉㄠ",
"dei":"ㄉㄟ","den":"ㄉㄣ","dia":"ㄉㄧㄚ","die":"ㄉㄧㄝ","diu":"ㄉㄧㄡ","dou":"ㄉㄡ",
"dui":"ㄉㄨㄟ","dun":"ㄉㄨㄣ","duo":"ㄉㄨㄛ","eng":"","fan":"ㄈㄢ","fei":"ㄈㄟ",
"fen":"ㄈㄣ","fou":"ㄈㄡ","gai":"ㄍㄞ","gan":"ㄍㄢ","gao":"ㄍㄠ","gei":"ㄍㄟ",
"gen":"ㄍㄣ","gin":"ㄍㄧㄣ","gou":"ㄍㄡ","gua":"ㄍㄨㄚ","gue":"ㄍㄨㄜ","gui":"ㄍㄨㄟ",
"gun":"ㄍㄨㄣ","guo":"ㄍㄨㄛ","hai":"ㄏㄞ","han":"ㄏㄢ","hao":"ㄏㄠ","hei":"ㄏㄟ",
"hen":"ㄏㄣ","hou":"ㄏㄡ","hua":"ㄏㄨㄚ","hui":"ㄏㄨㄟ","hun":"ㄏㄨㄣ","huo":"ㄏㄨㄛ",
"jia":"ㄐㄧㄚ","jie":"ㄐㄧㄝ","jin":"ㄐㄧㄣ","jiu":"ㄐㄧㄡ","jue":"ㄐㄩㄝ",
"jun":"ㄐㄩㄣ","kai":"ㄎㄞ","kan":"ㄎㄢ","kao":"ㄎㄠ","ken":"ㄎㄣ","kiu":"ㄎㄧㄡ",
"kou":"ㄎㄡ","kua":"ㄎㄨㄚ","kui":"ㄎㄨㄟ","kun":"ㄎㄨㄣ","kuo":"ㄎㄨㄛ","lai":"ㄌㄞ",
"lan":"ㄌㄢ","lao":"ㄌㄠ","lei":"ㄌㄟ","lia":"ㄌㄧㄚ","lie":"ㄌㄧㄝ","lin":"ㄌㄧㄣ",
"liu":"ㄌㄧㄡ","lou":"ㄌㄡ","lun":"ㄌㄨㄣ","luo":"ㄌㄨㄛ","lve":"ㄌㄩㄝ","mai":"ㄇㄞ",
"man":"ㄇㄢ","mao":"ㄇㄠ","mei":"ㄇㄟ","men":"ㄇㄣ","mie":"ㄇㄧㄝ","min":"ㄇㄧㄣ",
"miu":"ㄇㄧㄡ","mou":"ㄇㄡ","nai":"ㄋㄞ","nan":"ㄋㄢ","nao":"ㄋㄠ","nei":"ㄋㄟ",
"nen":"ㄋㄣ","nie":"ㄋㄧㄝ","nin":"ㄋㄧㄣ","niu":"ㄋㄧㄡ","nou":"ㄋㄡ","nui":"ㄋㄨㄟ",
"nun":"ㄋㄨㄣ","nuo":"ㄋㄨㄛ","nve":"ㄋㄩㄝ","pai":"ㄆㄞ","pan":"ㄆㄢ","pao":"ㄆㄠ",
"pei":"ㄆㄟ","pen":"ㄆㄣ","pia":"ㄆㄧㄚ","pie":"ㄆㄧㄝ","pin":"ㄆㄧㄣ","pou":"ㄆㄡ",
"qia":"ㄑㄧㄚ","qie":"ㄑㄧㄝ","qin":"ㄑㄧㄣ","qiu":"ㄑㄧㄡ","que":"ㄑㄩㄝ",
"qun":"ㄑㄩㄣ","ran":"ㄖㄢ","rao":"ㄖㄠ","ren":"ㄖㄣ","rou":"ㄖㄡ","rui":"ㄖㄨㄟ",
"run":"ㄖㄨㄣ","ruo":"ㄖㄨㄛ","sai":"ㄙㄞ","san":"ㄙㄢ","sao":"ㄙㄠ","sei":"ㄙㄟ",
"sen":"ㄙㄣ","sha":"ㄕㄚ","she":"ㄕㄜ","shi":"","shu":"ㄕㄨ","sou":"ㄙㄡ",
"sui":"ㄙㄨㄟ","sun":"ㄙㄨㄣ","suo":"ㄙㄨㄛ","tai":"ㄊㄞ","tan":"ㄊㄢ","tao":"ㄊㄠ",
"tie":"ㄊㄧㄝ","tou":"ㄊㄡ","tui":"ㄊㄨㄟ","tun":"ㄊㄨㄣ","tuo":"ㄊㄨㄛ",
"wai":"ㄨㄞ","wan":"ㄨㄢ","wei":"ㄨㄟ","wen":"ㄨㄣ","xia":"ㄒㄧㄚ","xie":"ㄒㄧㄝ",
"xin":"ㄒㄧㄣ","xiu":"ㄒㄧㄡ","xue":"ㄒㄩㄝ","xun":"ㄒㄩㄣ","yai":"ㄧㄞ",
"yan":"ㄧㄢ","yao":"ㄧㄠ","yin":"ㄧㄣ","you":"ㄧㄡ","yue":"ㄩㄝ","yun":"ㄩㄣ",
"zai":"ㄗㄞ","zan":"ㄗㄢ","zao":"ㄗㄠ","zei":"ㄗㄟ","zen":"ㄗㄣ","zha":"ㄓㄚ",
"zhe":"ㄓㄜ","zhi":"","zhu":"ㄓㄨ","zou":"ㄗㄡ","zui":"ㄗㄨㄟ","zun":"ㄗㄨㄣ",
"zuo":"ㄗㄨㄛ"},
"4":{"bang":"ㄅㄤ","beng":"ㄅㄥ","bian":"ㄅㄧㄢ","biao":"ㄅㄧㄠ","bing":"ㄅㄧㄥ",
"cang":"ㄘㄤ","ceng":"ㄘㄥ","chai":"ㄔㄞ","chan":"ㄔㄢ","chao":"ㄔㄠ","chen":"ㄔㄣ",
"chou":"ㄔㄡ","chua":"ㄔㄨㄚ","chui":"ㄔㄨㄟ","chun":"ㄔㄨㄣ","chuo":"ㄔㄨㄛ",
"cong":"ㄘㄨㄥ","cuan":"ㄘㄨㄢ","dang":"ㄉㄤ","deng":"ㄉㄥ","dian":"ㄉㄧㄢ",
"diao":"ㄉㄧㄠ","ding":"ㄉㄧㄥ","dong":"ㄉㄨㄥ","duan":"ㄉㄨㄢ","fang":"ㄈㄤ",
"feng":"ㄈㄥ","fiao":"ㄈㄧㄠ","fong":"ㄈㄨㄥ","gang":"ㄍㄤ","geng":"ㄍㄥ",
"giao":"ㄍㄧㄠ","gong":"ㄍㄨㄥ","guai":"ㄍㄨㄞ","guan":"ㄍㄨㄢ","hang":"ㄏㄤ",
"heng":"ㄏㄥ","hong":"ㄏㄨㄥ","huai":"ㄏㄨㄞ","huan":"ㄏㄨㄢ","jian":"ㄐㄧㄢ",
"jiao":"ㄐㄧㄠ","jing":"ㄐㄧㄥ","juan":"ㄐㄩㄢ","kang":"ㄎㄤ","keng":"ㄎㄥ",
"kong":"ㄎㄨㄥ","kuai":"ㄎㄨㄞ","kuan":"ㄎㄨㄢ","lang":"ㄌㄤ","leng":"ㄌㄥ",
"lian":"ㄌㄧㄢ","liao":"ㄌㄧㄠ","ling":"ㄌㄧㄥ","long":"ㄌㄨㄥ","luan":"ㄌㄨㄢ",
"lvan":"ㄌㄩㄢ","mang":"ㄇㄤ","meng":"ㄇㄥ","mian":"ㄇㄧㄢ","miao":"ㄇㄧㄠ",
"ming":"ㄇㄧㄥ","nang":"ㄋㄤ","neng":"ㄋㄥ","nian":"ㄋㄧㄢ","niao":"ㄋㄧㄠ",
"ning":"ㄋㄧㄥ","nong":"ㄋㄨㄥ","nuan":"ㄋㄨㄢ","pang":"ㄆㄤ","peng":"ㄆㄥ",
"pian":"ㄆㄧㄢ","piao":"ㄆㄧㄠ","ping":"ㄆㄧㄥ","qian":"ㄑㄧㄢ","qiao":"ㄑㄧㄠ",
"qing":"ㄑㄧㄥ","quan":"ㄑㄩㄢ","rang":"ㄖㄤ","reng":"ㄖㄥ","rong":"ㄖㄨㄥ",
"ruan":"ㄖㄨㄢ","sang":"ㄙㄤ","seng":"ㄙㄥ","shai":"ㄕㄞ","shan":"ㄕㄢ",
"shao":"ㄕㄠ","shei":"ㄕㄟ","shen":"ㄕㄣ","shou":"ㄕㄡ","shua":"ㄕㄨㄚ",
"shui":"ㄕㄨㄟ","shun":"ㄕㄨㄣ","shuo":"ㄕㄨㄛ","song":"ㄙㄨㄥ","suan":"ㄙㄨㄢ",
"tang":"ㄊㄤ","teng":"ㄊㄥ","tian":"ㄊㄧㄢ","tiao":"ㄊㄧㄠ","ting":"ㄊㄧㄥ",
"tong":"ㄊㄨㄥ","tuan":"ㄊㄨㄢ","wang":"ㄨㄤ","weng":"ㄨㄥ","xian":"ㄒㄧㄢ",
"xiao":"ㄒㄧㄠ","xing":"ㄒㄧㄥ","xuan":"ㄒㄩㄢ","yang":"ㄧㄤ","ying":"ㄧㄥ",
"yong":"ㄩㄥ","yuan":"ㄩㄢ","zang":"ㄗㄤ","zeng":"ㄗㄥ","zhai":"ㄓㄞ",
"zhan":"ㄓㄢ","zhao":"ㄓㄠ","zhei":"ㄓㄟ","zhen":"ㄓㄣ","zhou":"ㄓㄡ",
"zhua":"ㄓㄨㄚ","zhui":"ㄓㄨㄟ","zhun":"ㄓㄨㄣ","zhuo":"ㄓㄨㄛ",
"zong":"ㄗㄨㄥ","zuan":"ㄗㄨㄢ"},
"5":{"biang":"ㄅㄧㄤ","chang":"ㄔㄤ","cheng":"ㄔㄥ","chong":"ㄔㄨㄥ","chuai":"ㄔㄨㄞ",
"chuan":"ㄔㄨㄢ","duang":"ㄉㄨㄤ","guang":"ㄍㄨㄤ","huang":"ㄏㄨㄤ","jiang":"ㄐㄧㄤ",
"jiong":"ㄐㄩㄥ","kiang":"ㄎㄧㄤ","kuang":"ㄎㄨㄤ","liang":"ㄌㄧㄤ","niang":"ㄋㄧㄤ",
"qiang":"ㄑㄧㄤ","qiong":"ㄑㄩㄥ","shang":"ㄕㄤ","sheng":"ㄕㄥ","shuai":"ㄕㄨㄞ",
"shuan":"ㄕㄨㄢ","xiang":"ㄒㄧㄤ","xiong":"ㄒㄩㄥ","zhang":"ㄓㄤ","zheng":"ㄓㄥ",
"zhong":"ㄓㄨㄥ","zhuai":"ㄓㄨㄞ","zhuan":"ㄓㄨㄢ"},
"6":{"chuang":"ㄔㄨㄤ","shuang":"ㄕㄨㄤ","zhuang":"ㄓㄨㄤ"}
}
"""#

View File

@ -7,13 +7,11 @@
// requirements defined in MIT License.
import Megrez
import PinyinPhonaConverter
import Shared
public extension vChewingLM {
@frozen struct LMAssociates {
extension LMAssembly {
struct LMAssociates {
public private(set) var filePath: String?
var rangeMap: [String: [(Range<String.Index>, Int)]] = [:]
var rangeMap: [String: [(Range<String.Index>, Int)]] = [:] // Range index
var strData: String = ""
public var count: Int { rangeMap.count }
@ -48,8 +46,8 @@ public extension vChewingLM {
replaceData(textData: rawStrData)
} catch {
filePath = oldPath
vCLog("\(error)")
vCLog("↑ Exception happened when reading data at: \(path).")
vCLMLog("\(error)")
vCLMLog("↑ Exception happened when reading data at: \(path).")
return false
}
@ -93,28 +91,21 @@ public extension vChewingLM {
do {
try strData.write(toFile: filePath, atomically: true, encoding: .utf8)
} catch {
vCLog("Failed to save current database to: \(filePath)")
vCLMLog("Failed to save current database to: \(filePath)")
}
}
public func valuesFor(pair: Megrez.KeyValuePaired) -> [String] {
var pairs: [String] = []
if let arrRangeRecords: [(Range<String.Index>, Int)] = rangeMap[pair.toNGramKey] {
for (netaRange, index) in arrRangeRecords {
let availableResults = [rangeMap[pair.toNGramKey], rangeMap[pair.value]].compactMap { $0 }
availableResults.forEach { arrRangeRecords in
arrRangeRecords.forEach { netaRange, index in
let neta = strData[netaRange].split(separator: " ")
let theValue: String = .init(neta[index])
pairs.append(theValue)
}
}
if let arrRangeRecords: [(Range<String.Index>, Int)] = rangeMap[pair.value] {
for (netaRange, index) in arrRangeRecords {
let neta = strData[netaRange].split(separator: " ")
let theValue: String = .init(neta[index])
pairs.append(theValue)
}
}
var set = Set<String>()
return pairs.filter { set.insert($0).inserted }
return pairs.deduplicated
}
public func hasValuesFor(pair: Megrez.KeyValuePaired) -> Bool {
@ -123,3 +114,17 @@ public extension vChewingLM {
}
}
}
extension LMAssembly.LMAssociates {
var dictRepresented: [String: [String]] {
var result = [String: [String]]()
rangeMap.forEach { key, arrRangeRecords in
arrRangeRecords.forEach { netaRange, index in
let neta = strData[netaRange].split(separator: " ")
let theValue: String = .init(neta[index])
result[key, default: []].append(theValue)
}
}
return result
}
}

View File

@ -10,11 +10,10 @@
import Foundation
import LineReader
import Megrez
import Shared
public extension vChewingLM {
extension LMAssembly {
/// 便使
@frozen struct LMCassette {
struct LMCassette {
public private(set) var filePath: String?
public private(set) var nameShort: String = ""
public private(set) var nameENG: String = ""
@ -40,12 +39,13 @@ public extension vChewingLM {
public private(set) var areCandidateKeysShiftHeld: Bool = false
public private(set) var supplyQuickResults: Bool = false
public private(set) var supplyPartiallyMatchedResults: Bool = false
public var candidateKeysValidator: (String) -> Bool = { _ in false }
/// 西 - NORM
private var norm = 0.0
}
}
public extension vChewingLM.LMCassette {
extension LMAssembly.LMCassette {
/// 西 - fscale
private static let fscale = 2.7
///
@ -86,7 +86,7 @@ public extension vChewingLM.LMCassette {
if FileManager.default.fileExists(atPath: path) {
do {
guard let fileHandle = FileHandle(forReadingAtPath: path) else {
throw vChewingLM.FileErrors.fileHandleError("")
throw LMAssembly.FileErrors.fileHandleError("")
}
let lineReader = try LineReader(file: fileHandle)
var theMaxKeyLength = 1
@ -195,7 +195,7 @@ public extension vChewingLM.LMCassette {
// Post process.
// Package 便 J / K
//
if CandidateKey.validate(keys: selectionKeys) != nil { selectionKeys = "1234567890" }
if !candidateKeysValidator(selectionKeys) { selectionKeys = "1234567890" }
if !keysUsedInCharDef.intersection(selectionKeys.map(\.description)).isEmpty {
areCandidateKeysShiftHeld = true
}
@ -204,10 +204,10 @@ public extension vChewingLM.LMCassette {
filePath = path
return true
} catch {
vCLog("CIN Loading Failed: File Access Error.")
vCLMLog("CIN Loading Failed: File Access Error.")
}
} else {
vCLog("CIN Loading Failed: File Missing.")
vCLMLog("CIN Loading Failed: File Missing.")
}
filePath = oldPath
return false

View File

@ -7,15 +7,13 @@
// requirements defined in MIT License.
import Megrez
import PinyinPhonaConverter
import Shared
public extension vChewingLM {
extension LMAssembly {
/// LMCore LMCoreEX range
/// range strData
/// C++ ParselessLM Swift
/// For
@frozen struct LMCoreEX {
struct LMCoreEX {
public private(set) var filePath: String?
/// 便 strData
var rangeMap: [String: [Range<String.Index>]] = [:]
@ -81,8 +79,8 @@ public extension vChewingLM {
replaceData(textData: rawStrData)
} catch {
filePath = oldPath
vCLog("\(error)")
vCLog("↑ Exception happened when reading data at: \(path).")
vCLMLog("\(error)")
vCLMLog("↑ Exception happened when reading data at: \(path).")
return false
}
@ -133,7 +131,7 @@ public extension vChewingLM {
}
try dataToWrite.write(toFile: filePath, atomically: true, encoding: .utf8)
} catch {
vCLog("Failed to save current database to: \(filePath)")
vCLMLog("Failed to save current database to: \(filePath)")
}
}
@ -150,7 +148,7 @@ public extension vChewingLM {
strDump += addline
}
}
vCLog(strDump)
vCLMLog(strDump)
}
/// strData
@ -186,3 +184,15 @@ public extension vChewingLM {
}
}
}
extension LMAssembly.LMCoreEX {
var dictRepresented: [String: [String]] {
var result = [String: [String]]()
rangeMap.forEach { key, arrValueRanges in
result[key, default: []] = arrValueRanges.map { currentRange in
strData[currentRange].description
}
}
return result
}
}

View File

@ -7,67 +7,36 @@
// requirements defined in MIT License.
import Foundation
import Shared
public extension vChewingLM {
@frozen struct LMPlainBopomofo {
public private(set) var filePath: String?
var dataMap: [String: String] = [:]
extension LMAssembly {
struct LMPlainBopomofo {
@usableFromInline typealias DataMap = [String: [String: String]]
let dataMap: DataMap
public var count: Int { dataMap.count }
public init() {
dataMap = [:]
do {
let rawData = jsnEtenDosSequence.data(using: .utf8) ?? .init([])
let rawJSON = try JSONDecoder().decode([String: [String: String]].self, from: rawData)
dataMap = rawJSON
} catch {
vCLMLog("\(error)")
vCLMLog("↑ Exception happened when parsing raw JSON sequence data from vChewing LMAssembly.")
dataMap = [:]
}
}
public var isLoaded: Bool { !dataMap.isEmpty }
@discardableResult public mutating func open(_ path: String) -> Bool {
if isLoaded { return false }
let oldPath = filePath
filePath = nil
do {
let rawData = try Data(contentsOf: URL(fileURLWithPath: path))
if let rawJSON = try? JSONSerialization.jsonObject(with: rawData) as? [String: String] {
dataMap = rawJSON
} else {
filePath = oldPath
vCLog("↑ Exception happened when reading JSON file at: \(path).")
return false
}
} catch {
filePath = oldPath
vCLog("\(error)")
vCLog("↑ Exception happened when reading JSON file at: \(path).")
return false
}
filePath = path
return true
}
public mutating func clear() {
filePath = nil
dataMap.removeAll()
}
public func saveData() {
guard let filePath = filePath, let plistURL = URL(string: filePath) else { return }
do {
let plistData = try PropertyListSerialization.data(fromPropertyList: dataMap, format: .binary, options: 0)
try plistData.write(to: plistURL)
} catch {
vCLog("Failed to save current database to: \(filePath)")
}
}
public func valuesFor(key: String) -> [String] {
public func valuesFor(key: String, isCHS: Bool) -> [String] {
var pairs: [String] = []
if let arrRangeRecords: String = dataMap[key]?.trimmingCharacters(in: .newlines) {
let subKey = isCHS ? "S" : "T"
if let arrRangeRecords: String = dataMap[key]?[subKey] {
pairs.append(contentsOf: arrRangeRecords.map(\.description))
}
return pairs.deduplicated
//
return pairs
}
public func hasValuesFor(key: String) -> Bool { dataMap.keys.contains(key) }

View File

@ -6,10 +6,8 @@
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import Shared
public extension vChewingLM {
@frozen struct LMReplacements {
extension LMAssembly {
struct LMReplacements {
public private(set) var filePath: String?
var rangeMap: [String: Range<String.Index>] = [:]
var strData: String = ""
@ -35,8 +33,8 @@ public extension vChewingLM {
replaceData(textData: rawStrData)
} catch {
filePath = oldPath
vCLog("\(error)")
vCLog("↑ Exception happened when reading data at: \(path).")
vCLMLog("\(error)")
vCLMLog("↑ Exception happened when reading data at: \(path).")
return false
}
@ -72,7 +70,7 @@ public extension vChewingLM {
do {
try strData.write(toFile: filePath, atomically: true, encoding: .utf8)
} catch {
vCLog("Failed to save current database to: \(filePath)")
vCLMLog("Failed to save current database to: \(filePath)")
}
}
@ -81,7 +79,7 @@ public extension vChewingLM {
for entry in rangeMap {
strDump += strData[entry.value] + "\n"
}
vCLog(strDump)
vCLMLog(strDump)
}
public func valuesFor(key: String) -> String {
@ -100,3 +98,13 @@ public extension vChewingLM {
}
}
}
extension LMAssembly.LMReplacements {
var dictRepresented: [String: String] {
var result = [String: String]()
rangeMap.forEach { key, valueRange in
result[key] = strData[valueRange].description
}
return result
}
}

View File

@ -1,76 +0,0 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import Foundation
import Shared
public extension vChewingLM {
@frozen struct LMRevLookup {
public private(set) var dataMap: [String: [String]] = [:]
public private(set) var filePath: String = ""
public init(data dictData: (dict: [String: [String]]?, path: String)) {
guard let theDict = dictData.dict else {
vCLog("↑ Exception happened when reading JSON file at: \(dictData.path).")
return
}
filePath = dictData.path
dataMap = theDict
}
public init(path: String) {
if path.isEmpty { return }
do {
let rawData = try Data(contentsOf: URL(fileURLWithPath: path))
if let rawJSON = try? JSONSerialization.jsonObject(with: rawData) as? [String: [String]] {
dataMap = rawJSON
} else {
vCLog("↑ Exception happened when reading JSON file at: \(path).")
return
}
} catch {
vCLog("↑ Exception happened when reading JSON file at: \(path).")
return
}
filePath = path
}
public func query(with kanji: String) -> [String]? {
guard let resultData = dataMap[kanji] else { return nil }
let resultArray = resultData.compactMap {
let result = restorePhonabetFromASCII($0)
return result.isEmpty ? nil : result
}
return resultArray.isEmpty ? nil : resultArray
}
///
///
/// ASCII
/// - parameters:
/// - incoming:
func restorePhonabetFromASCII(_ incoming: String) -> String {
var strOutput = incoming
if !strOutput.contains("_") {
for entry in Self.dicPhonabet4ASCII {
strOutput = strOutput.replacingOccurrences(of: entry.key, with: entry.value)
}
}
return strOutput
}
// MARK: - Constants
static let dicPhonabet4ASCII: [String: String] = [
"b": "", "p": "", "m": "", "f": "", "d": "", "t": "", "n": "", "l": "", "g": "", "k": "", "h": "",
"j": "", "q": "", "x": "", "Z": "", "C": "", "S": "", "r": "", "z": "", "c": "", "s": "", "i": "",
"u": "", "v": "", "a": "", "o": "", "e": "", "E": "", "B": "", "P": "", "M": "", "F": "", "D": "",
"T": "", "N": "", "L": "", "R": "", "2": "ˊ", "3": "ˇ", "4": "ˋ", "5": "˙",
]
}
}

View File

@ -9,74 +9,41 @@
import Foundation
import Megrez
import Shared
public extension vChewingLM {
// MARK: - Public Types.
public extension LMAssembly {
struct OverrideSuggestion {
public var candidates = [(String, Megrez.Unigram)]()
public var forceHighScoreOverride = false
public var isEmpty: Bool { candidates.isEmpty }
}
}
// MARK: - LMUserOverride Class Definition.
extension LMAssembly {
class LMUserOverride {
// MARK: - Main
var mutCapacity: Int
var mutDecayExponent: Double
var mutLRUList: [KeyObservationPair] = []
var mutLRUMap: [String: KeyObservationPair] = [:]
let kDecayThreshold: Double = 1.0 / 1_048_576.0 //
var fileSaveLocationURL: URL
var fileSaveLocationURL: URL?
public static let kObservedOverrideHalfLife: Double = 3600.0 * 6 // 6
public init(capacity: Int = 500, decayConstant: Double = LMUserOverride.kObservedOverrideHalfLife, dataURL: URL) {
public init(capacity: Int = 500, decayConstant: Double = LMUserOverride.kObservedOverrideHalfLife, dataURL: URL? = nil) {
mutCapacity = max(capacity, 1) // Ensures that this integer value is always > 0.
mutDecayExponent = log(0.5) / decayConstant
fileSaveLocationURL = dataURL
}
public func performObservation(
walkedBefore: [Megrez.Node], walkedAfter: [Megrez.Node],
cursor: Int, timestamp: Double, saveCallback: @escaping () -> Void
) {
//
guard !walkedAfter.isEmpty, !walkedBefore.isEmpty else { return }
guard walkedBefore.totalKeyCount == walkedAfter.totalKeyCount else { return }
//
var actualCursor = 0
guard let currentNode = walkedAfter.findNode(at: cursor, target: &actualCursor) else { return }
// 使
guard currentNode.spanLength <= 3 else { return }
//
guard actualCursor > 0 else { return } //
let currentNodeIndex = actualCursor
actualCursor -= 1
var prevNodeIndex = 0
guard let prevNode = walkedBefore.findNode(at: actualCursor, target: &prevNodeIndex) else { return }
let forceHighScoreOverride: Bool = currentNode.spanLength > prevNode.spanLength
let breakingUp = currentNode.spanLength == 1 && prevNode.spanLength > 1
let targetNodeIndex = breakingUp ? currentNodeIndex : prevNodeIndex
let key: String = vChewingLM.LMUserOverride.formObservationKey(
walkedNodes: walkedAfter, headIndex: targetNodeIndex
)
guard !key.isEmpty else { return }
doObservation(
key: key, candidate: currentNode.currentUnigram.value, timestamp: timestamp,
forceHighScoreOverride: forceHighScoreOverride, saveCallback: { saveCallback() }
)
}
public func fetchSuggestion(
currentWalk: [Megrez.Node], cursor: Int, timestamp: Double
) -> Suggestion {
var headIndex = 0
guard let nodeIter = currentWalk.findNode(at: cursor, target: &headIndex) else { return .init() }
let key = vChewingLM.LMUserOverride.formObservationKey(walkedNodes: currentWalk, headIndex: headIndex)
return getSuggestion(key: key, timestamp: timestamp, headReading: nodeIter.joinedKey())
}
}
}
// MARK: - Private Structures
extension vChewingLM.LMUserOverride {
extension LMAssembly.LMUserOverride {
enum OverrideUnit: CodingKey { case count, timestamp, forceHighScoreOverride }
enum ObservationUnit: CodingKey { case count, overrides }
enum KeyObservationPairUnit: CodingKey { case key, observation }
@ -153,10 +120,52 @@ extension vChewingLM.LMUserOverride {
}
}
// MARK: - Hash and Dehash the entire UOM data, etc.
// MARK: - Internal Methods in LMAssembly.
public extension vChewingLM.LMUserOverride {
func bleachSpecifiedSuggestions(targets: [String], saveCallback: @escaping () -> Void) {
extension LMAssembly.LMUserOverride {
func performObservation(
walkedBefore: [Megrez.Node], walkedAfter: [Megrez.Node],
cursor: Int, timestamp: Double, saveCallback: (() -> Void)? = nil
) {
//
guard !walkedAfter.isEmpty, !walkedBefore.isEmpty else { return }
guard walkedBefore.totalKeyCount == walkedAfter.totalKeyCount else { return }
//
var actualCursor = 0
guard let currentNode = walkedAfter.findNode(at: cursor, target: &actualCursor) else { return }
// 使
guard currentNode.spanLength <= 3 else { return }
//
guard actualCursor > 0 else { return } //
let currentNodeIndex = actualCursor
actualCursor -= 1
var prevNodeIndex = 0
guard let prevNode = walkedBefore.findNode(at: actualCursor, target: &prevNodeIndex) else { return }
let forceHighScoreOverride: Bool = currentNode.spanLength > prevNode.spanLength
let breakingUp = currentNode.spanLength == 1 && prevNode.spanLength > 1
let targetNodeIndex = breakingUp ? currentNodeIndex : prevNodeIndex
let key: String = LMAssembly.LMUserOverride.formObservationKey(
walkedNodes: walkedAfter, headIndex: targetNodeIndex
)
guard !key.isEmpty else { return }
doObservation(
key: key, candidate: currentNode.currentUnigram.value, timestamp: timestamp,
forceHighScoreOverride: forceHighScoreOverride, saveCallback: saveCallback
)
}
func fetchSuggestion(
currentWalk: [Megrez.Node], cursor: Int, timestamp: Double
) -> LMAssembly.OverrideSuggestion {
var headIndex = 0
guard let nodeIter = currentWalk.findNode(at: cursor, target: &headIndex) else { return .init() }
let key = LMAssembly.LMUserOverride.formObservationKey(walkedNodes: currentWalk, headIndex: headIndex)
return getSuggestion(key: key, timestamp: timestamp, headReading: nodeIter.joinedKey())
}
func bleachSpecifiedSuggestions(targets: [String], saveCallback: (() -> Void)? = nil) {
if targets.isEmpty { return }
for neta in mutLRUMap {
for target in targets {
@ -166,82 +175,86 @@ public extension vChewingLM.LMUserOverride {
}
}
resetMRUList()
saveCallback()
saveCallback?() ?? saveData()
}
/// LRU
func bleachUnigrams(saveCallback: @escaping () -> Void) {
func bleachUnigrams(saveCallback: (() -> Void)? = nil) {
for key in mutLRUMap.keys {
if !key.contains("(),()") { continue }
mutLRUMap.removeValue(forKey: key)
}
resetMRUList()
saveCallback()
saveCallback?() ?? saveData()
}
internal func resetMRUList() {
func resetMRUList() {
mutLRUList.removeAll()
for neta in mutLRUMap.reversed() {
mutLRUList.append(neta.value)
}
}
func clearData(withURL fileURL: URL) {
func clearData(withURL fileURL: URL? = nil) {
mutLRUMap = .init()
mutLRUList = .init()
do {
let nullData = "{}"
guard let fileURL = fileURL ?? fileSaveLocationURL else {
throw UOMError(rawValue: "given fileURL is invalid or nil.")
}
try nullData.write(to: fileURL, atomically: false, encoding: .utf8)
} catch {
vCLog("UOM Error: Unable to clear data. Details: \(error)")
vCLMLog("UOM Error: Unable to clear the data in the UOM file. Details: \(error)")
return
}
}
func saveData(toURL fileURL: URL? = nil) {
guard let fileURL: URL = fileURL ?? fileSaveLocationURL else {
vCLMLog("UOM saveData() failed. At least the file Save URL is not set for the current UOM.")
return
}
// 使 JSONSerialization
let encoder = JSONEncoder()
do {
guard let jsonData = try? encoder.encode(mutLRUMap) else { return }
let fileURL: URL = fileURL ?? fileSaveLocationURL
try jsonData.write(to: fileURL, options: .atomic)
} catch {
vCLog("UOM Error: Unable to save data, abort saving. Details: \(error)")
vCLMLog("UOM Error: Unable to save data, abort saving. Details: \(error)")
return
}
}
func loadData(fromURL fileURL: URL) {
func loadData(fromURL fileURL: URL? = nil) {
guard let fileURL: URL = fileURL ?? fileSaveLocationURL else {
vCLMLog("UOM loadData() failed. At least the file Load URL is not set for the current UOM.")
return
}
// 使 JSONSerialization
let decoder = JSONDecoder()
do {
let data = try Data(contentsOf: fileURL, options: .mappedIfSafe)
if ["", "{}"].contains(String(data: data, encoding: .utf8)) { return }
guard let jsonResult = try? decoder.decode([String: KeyObservationPair].self, from: data) else {
vCLog("UOM Error: Read file content type invalid, abort loading.")
vCLMLog("UOM Error: Read file content type invalid, abort loading.")
return
}
mutLRUMap = jsonResult
resetMRUList()
} catch {
vCLog("UOM Error: Unable to read file or parse the data, abort loading. Details: \(error)")
vCLMLog("UOM Error: Unable to read file or parse the data, abort loading. Details: \(error)")
return
}
}
struct Suggestion {
public var candidates = [(String, Megrez.Unigram)]()
public var forceHighScoreOverride = false
public var isEmpty: Bool { candidates.isEmpty }
}
}
// MARK: - Private Methods
// MARK: - Other Non-Public Internal Methods
extension vChewingLM.LMUserOverride {
extension LMAssembly.LMUserOverride {
func doObservation(
key: String, candidate: String, timestamp: Double, forceHighScoreOverride: Bool,
saveCallback: @escaping () -> Void
saveCallback: (() -> Void)?
) {
guard mutLRUMap[key] != nil else {
var observation: Observation = .init()
@ -257,8 +270,8 @@ extension vChewingLM.LMUserOverride {
mutLRUMap.removeValue(forKey: mutLRUList[mutLRUList.endIndex - 1].key)
mutLRUList.removeLast()
}
vCLog("UOM: Observation finished with new observation: \(key)")
saveCallback()
vCLMLog("UOM: Observation finished with new observation: \(key)")
saveCallback?() ?? saveData()
return
}
// decayCallback
@ -268,12 +281,12 @@ extension vChewingLM.LMUserOverride {
)
mutLRUList.insert(theNeta, at: 0)
mutLRUMap[key] = theNeta
vCLog("UOM: Observation finished with existing observation: \(key)")
saveCallback()
vCLMLog("UOM: Observation finished with existing observation: \(key)")
saveCallback?() ?? saveData()
}
}
func getSuggestion(key: String, timestamp: Double, headReading: String) -> Suggestion {
func getSuggestion(key: String, timestamp: Double, headReading: String) -> LMAssembly.OverrideSuggestion {
guard !key.isEmpty, let kvPair = mutLRUMap[key] else { return .init() }
let observation: Observation = kvPair.observation
var candidates: [(String, Megrez.Unigram)] = .init()
@ -386,3 +399,10 @@ extension vChewingLM.LMUserOverride {
return result
}
}
struct UOMError: LocalizedError {
var rawValue: String
var errorDescription: String? {
NSLocalizedString("rawValue", comment: "")
}
}

View File

@ -1,9 +1,5 @@
//
// File.swift
//
//
// Created by ShikiSuen on 2023/11/26.
//
// libTaBE (http://sourceforge.net/projects/libtabe/)
// (2002 ). 1999 Pai-Hsiang Hsiao BSD
import Foundation
@ -29,6 +25,8 @@ INSERT INTO DATA_MAIN VALUES('de5','-3.516024 的\t-7.427179 得','-3.516024 的
INSERT INTO DATA_MAIN VALUES('di2','-3.516024 ','-3.516024 ',NULL,NULL,NULL,NULL);
INSERT INTO DATA_MAIN VALUES('di4','-3.516024 ','-3.516024 ',NULL,NULL,NULL,NULL);
INSERT INTO DATA_MAIN VALUES('duP3','-9.544 ','-9.544 ','\t䇏\t𦞙\t謉\t𠡒\t𡑈\t𥫉\t𦞱\t𧫏\t𩛔','','','');
INSERT INTO DATA_MAIN VALUES('uP','-6.0 ','-6.0 ',NULL,NULL,NULL,NULL); /* CNS */
INSERT INTO DATA_MAIN VALUES('uP2','-6.0 ','-6.0 ','-6.0 ',NULL,NULL,NULL); /* CNS */
INSERT INTO DATA_MAIN VALUES('fL','-11.0 🐝','-11.0 🐝',NULL,NULL,NULL,NULL);
INSERT INTO DATA_MAIN VALUES('gM','-7.171551 \t-11.92872 \t-13.624335 \t-12.390804 ','-7.171551 \t-11.92872 \t-13.624335 \t-12.390804 ',NULL,NULL,NULL,NULL);
INSERT INTO DATA_MAIN VALUES('gM-ke-ji4','-9.842421 ','-9.842421 ',NULL,NULL,NULL,NULL);

View File

@ -7,10 +7,9 @@
// requirements defined in MIT License.
import Foundation
import Shared
import SQLite3
public enum vChewingLM {
public enum LMAssembly {
enum FileErrors: Error {
case fileHandleError(String)
}
@ -56,7 +55,7 @@ extension Array where Element == String {
sqlite3_prepare_v2(ptrDB, strStmt, -1, &ptrStmt, nil) == SQLITE_OK && sqlite3_step(ptrStmt) == SQLITE_DONE
}
guard thisResult else {
vCLog("SQL Query Error. Statement: \(strStmt)")
vCLMLog("SQL Query Error. Statement: \(strStmt)")
return false
}
}
@ -83,3 +82,13 @@ func performStatementSansResult(_ handler: (inout OpaquePointer?) -> Void) {
}
handler(&ptrStmt)
}
func vCLMLog(_ strPrint: StringLiteralType) {
guard let toLog = UserDefaults.standard.object(forKey: "_DebugMode") as? Bool else {
NSLog("vChewingDebug: %@", strPrint)
return
}
if toLog {
NSLog("vChewingDebug: %@", strPrint)
}
}

View File

@ -57,8 +57,8 @@ final class InputTokenTests: XCTestCase {
}
func testGeneratedResultsFromLMInstantiator() throws {
let instance = vChewingLM.LMInstantiator(isCHS: true)
XCTAssertTrue(vChewingLM.LMInstantiator.connectToTestSQLDB())
let instance = LMAssembly.LMInstantiator(isCHS: true)
XCTAssertTrue(LMAssembly.LMInstantiator.connectToTestSQLDB())
instance.setOptions { config in
config.isCNSEnabled = false
config.isSymbolEnabled = false
@ -70,6 +70,6 @@ final class InputTokenTests: XCTestCase {
)
let x = instance.unigramsFor(keyArray: ["ㄐㄧㄣ", "ㄊㄧㄢ", "ㄖˋ", "ㄑㄧˊ"]).description
print(x)
vChewingLM.LMInstantiator.disconnectSQLDB()
LMAssembly.LMInstantiator.disconnectSQLDB()
}
}

View File

@ -20,7 +20,7 @@ private let testDataPath: String = packageRootPath + "/Tests/TestCINData/"
final class LMCassetteTests: XCTestCase {
func testCassetteLoadWubi86() throws {
let pathCINFile = testDataPath + "wubi.cin"
var lmCassette = vChewingLM.LMCassette()
var lmCassette = LMAssembly.LMCassette()
NSLog("LMCassette: Start loading CIN.")
lmCassette.open(pathCINFile)
NSLog("LMCassette: Finished loading CIN. Entries: \(lmCassette.count)")
@ -41,7 +41,7 @@ final class LMCassetteTests: XCTestCase {
func testCassetteLoadArray30() throws {
let pathCINFile = testDataPath + "array30.cin2"
var lmCassette = vChewingLM.LMCassette()
var lmCassette = LMAssembly.LMCassette()
NSLog("LMCassette: Start loading CIN.")
lmCassette.open(pathCINFile)
NSLog("LMCassette: Finished loading CIN. Entries: \(lmCassette.count)")

View File

@ -38,7 +38,7 @@ private let sampleData: String = #"""
final class LMCoreEXTests: XCTestCase {
func testLMCoreEXAsFactoryCoreDict() throws {
var lmTest = vChewingLM.LMCoreEX(
var lmTest = LMAssembly.LMCoreEX(
reverse: false, consolidate: false, defaultScore: 0, forceDefaultScore: false
)
lmTest.replaceData(textData: sampleData)

View File

@ -22,8 +22,8 @@ private let expectedReverseLookupResults: [String] = [
final class LMInstantiatorSQLTests: XCTestCase {
func testSQL() throws {
let instance = vChewingLM.LMInstantiator(isCHS: true)
XCTAssertTrue(vChewingLM.LMInstantiator.connectToTestSQLDB())
let instance = LMAssembly.LMInstantiator(isCHS: true)
XCTAssertTrue(LMAssembly.LMInstantiator.connectToTestSQLDB())
instance.setOptions { config in
config.isCNSEnabled = false
config.isSymbolEnabled = false
@ -41,7 +41,24 @@ final class LMInstantiatorSQLTests: XCTestCase {
XCTAssertEqual(instance.unigramsFor(keyArray: strRefutationKey).count, 10)
XCTAssertEqual(instance.unigramsFor(keyArray: strBoobsKey).last?.description, "(☉☉,-13.0)")
//
XCTAssertEqual(vChewingLM.LMInstantiator.getFactoryReverseLookupData(with: ""), expectedReverseLookupResults)
vChewingLM.LMInstantiator.disconnectSQLDB()
XCTAssertEqual(LMAssembly.LMInstantiator.getFactoryReverseLookupData(with: ""), expectedReverseLookupResults)
LMAssembly.LMInstantiator.disconnectSQLDB()
}
func testCNSMask() throws {
let instance = LMAssembly.LMInstantiator(isCHS: false)
XCTAssertTrue(LMAssembly.LMInstantiator.connectToTestSQLDB())
instance.setOptions { config in
config.isCNSEnabled = false
config.isSymbolEnabled = false
config.filterNonCNSReadings = false
}
XCTAssertEqual(instance.unigramsFor(keyArray: ["ㄨㄟ"]).description, "[(危,-6.0)]")
XCTAssertEqual(instance.unigramsFor(keyArray: ["ㄨㄟˊ"]).description, "[(危,-6.0)]")
instance.setOptions { config in
config.filterNonCNSReadings = true
}
XCTAssertEqual(instance.unigramsFor(keyArray: ["ㄨㄟ"]).description, "[]")
XCTAssertEqual(instance.unigramsFor(keyArray: ["ㄨㄟˊ"]).description, "[(危,-6.0)]")
}
}

View File

@ -0,0 +1,36 @@
//// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// StringView Ranges extension by (c) 2022 and onwards Isaac Xen (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import Foundation
import XCTest
@testable import LangModelAssembly
final class LMPlainBPMFTests: XCTestCase {
func testLMPlainBPMFDataQuery() throws {
let instance1 = LMAssembly.LMInstantiator(isCHS: false).setOptions { config in
config.isSCPCEnabled = true
}
var liu2 = instance1.unigramsFor(keyArray: ["ㄌㄧㄡˊ"]).map(\.value).prefix(3)
var bao3 = instance1.unigramsFor(keyArray: ["ㄅㄠˇ"]).map(\.value).prefix(3)
var jie2 = instance1.unigramsFor(keyArray: ["ㄐㄧㄝˊ"]).map(\.value).prefix(3)
XCTAssertEqual(liu2, ["", "", ""])
XCTAssertEqual(bao3, ["", "", ""])
XCTAssertEqual(jie2, ["", "", ""])
let instance2 = LMAssembly.LMInstantiator(isCHS: true).setOptions { config in
config.isSCPCEnabled = true
}
liu2 = instance2.unigramsFor(keyArray: ["ㄌㄧㄡˊ"]).map(\.value).prefix(3)
bao3 = instance2.unigramsFor(keyArray: ["ㄅㄠˇ"]).map(\.value).prefix(3)
jie2 = instance2.unigramsFor(keyArray: ["ㄐㄧㄝˊ"]).map(\.value).prefix(3)
XCTAssertEqual(liu2, ["", "", ""])
XCTAssertEqual(bao3, ["", "", ""])
XCTAssertEqual(jie2, ["", "", ""])
}
}

View File

@ -17,12 +17,12 @@ private let halfLife: Double = 5400
private let nullURL = URL(fileURLWithPath: "/dev/null")
final class LMUserOverrideTests: XCTestCase {
private func observe(who uom: vChewingLM.LMUserOverride, key: String, candidate: String, timestamp stamp: Double) {
private func observe(who uom: LMAssembly.LMUserOverride, key: String, candidate: String, timestamp stamp: Double) {
uom.doObservation(key: key, candidate: candidate, timestamp: stamp, forceHighScoreOverride: false, saveCallback: {})
}
func testUOM_1_BasicOps() throws {
let uom = vChewingLM.LMUserOverride(capacity: capacity, decayConstant: Double(halfLife), dataURL: nullURL)
let uom = LMAssembly.LMUserOverride(capacity: capacity, decayConstant: Double(halfLife), dataURL: nullURL)
let key = "((ㄕㄣˊ-ㄌㄧˇ-ㄌㄧㄥˊ-ㄏㄨㄚˊ,神里綾華),(ㄉㄜ˙,的),ㄍㄡˇ)"
let headReading = "ㄍㄡˇ"
let expectedSuggestion = ""
@ -45,7 +45,7 @@ final class LMUserOverrideTests: XCTestCase {
}
func testUOM_2_NewestAgainstRepeatedlyUsed() throws {
let uom = vChewingLM.LMUserOverride(capacity: capacity, decayConstant: Double(halfLife), dataURL: nullURL)
let uom = LMAssembly.LMUserOverride(capacity: capacity, decayConstant: Double(halfLife), dataURL: nullURL)
let key = "((ㄕㄣˊ-ㄌㄧˇ-ㄌㄧㄥˊ-ㄏㄨㄚˊ,神里綾華),(ㄉㄜ˙,的),ㄍㄡˇ)"
let headReading = "ㄍㄡˇ"
let valRepeatedlyUsed = "" //
@ -74,7 +74,7 @@ final class LMUserOverrideTests: XCTestCase {
let b = (key: "((ㄆㄞˋ-ㄇㄥˊ,派蒙),(ㄉㄜ˙,的),ㄐㄧㄤˇ-ㄐㄧㄣ)", value: "伙食費", head: "ㄏㄨㄛˇ-ㄕˊ-ㄈㄟˋ")
let c = (key: "((ㄍㄨㄛˊ-ㄅㄥ,國崩),(ㄉㄜ˙,的),ㄇㄠˋ-ㄗ˙)", value: "帽子", head: "ㄇㄠˋ-ㄗ˙")
let d = (key: "((ㄌㄟˊ-ㄉㄧㄢˋ-ㄐㄧㄤ-ㄐㄩㄣ,雷電將軍),(ㄉㄜ˙,的),ㄐㄧㄠˇ-ㄔㄡˋ)", value: "腳臭", head: "ㄐㄧㄠˇ-ㄔㄡˋ")
let uom = vChewingLM.LMUserOverride(capacity: 2, decayConstant: Double(halfLife), dataURL: nullURL)
let uom = LMAssembly.LMUserOverride(capacity: 2, decayConstant: Double(halfLife), dataURL: nullURL)
observe(who: uom, key: a.key, candidate: a.value, timestamp: nowTimeStamp)
observe(who: uom, key: b.key, candidate: b.value, timestamp: nowTimeStamp + halfLife * 1)
observe(who: uom, key: c.key, candidate: c.value, timestamp: nowTimeStamp + halfLife * 2)

View File

@ -13,7 +13,7 @@ import XCTest
final class LMInstantiatorNumericPadTests: XCTestCase {
func testSQL() throws {
let instance = vChewingLM.LMInstantiator(isCHS: true)
let instance = LMAssembly.LMInstantiator(isCHS: true)
instance.setOptions { config in
config.numPadFWHWStatus = nil
}

View File

@ -17,10 +17,12 @@ let package = Package(
.package(path: "../HangarRash_SwiftyCapsLockToggler"),
.package(path: "../Jad_BookmarkManager"),
.package(path: "../Qwertyyb_ShiftKeyUpChecker"),
.package(path: "../vChewing_BrailleSputnik"),
.package(path: "../vChewing_CandidateWindow"),
.package(path: "../vChewing_CocoaExtension"),
.package(path: "../vChewing_OSFrameworkImpl"),
.package(path: "../vChewing_Hotenka"),
.package(path: "../vChewing_IMKUtils"),
.package(path: "../vChewing_KimoDataReader"),
.package(path: "../vChewing_LangModelAssembly"),
.package(path: "../vChewing_Megrez"),
.package(path: "../vChewing_NotifierUI"),
@ -37,12 +39,14 @@ let package = Package(
.target(
name: "MainAssembly",
dependencies: [
.product(name: "BrailleSputnik", package: "vChewing_BrailleSputnik"),
.product(name: "BookmarkManager", package: "Jad_BookmarkManager"),
.product(name: "CandidateWindow", package: "vChewing_CandidateWindow"),
.product(name: "CocoaExtension", package: "vChewing_CocoaExtension"),
.product(name: "OSFrameworkImpl", package: "vChewing_OSFrameworkImpl"),
.product(name: "FolderMonitor", package: "DanielGalasko_FolderMonitor"),
.product(name: "Hotenka", package: "vChewing_Hotenka"),
.product(name: "IMKUtils", package: "vChewing_IMKUtils"),
.product(name: "KimoDataReader", package: "vChewing_KimoDataReader"),
.product(name: "LangModelAssembly", package: "vChewing_LangModelAssembly"),
.product(name: "Megrez", package: "vChewing_Megrez"),
.product(name: "NotifierUI", package: "vChewing_NotifierUI"),

View File

@ -7,6 +7,7 @@
// requirements defined in MIT License.
import AppKit
import Shared
import SwiftUI
public class CtlAboutUI: NSWindowController, NSWindowDelegate {

View File

@ -87,7 +87,11 @@ public class VwrAboutCocoa: NSViewController {
)
NSView()
}
NSStackView.build(.vertical, width: 114) {
var verticalButtonStackSpacing: CGFloat? = 4
if #unavailable(macOS 10.10) {
verticalButtonStackSpacing = nil
}
NSStackView.build(.vertical, spacing: verticalButtonStackSpacing, width: 114) {
addKeyEquivalent(
NSButton(
"i18n:aboutWindow.OK_BUTTON",

View File

@ -7,6 +7,7 @@
// requirements defined in MIT License.
import AppKit
import Shared
import SwiftUI
public struct VwrAboutUI {

View File

@ -47,7 +47,6 @@ extension AppDelegate {
// initUserLangModels()
if PrefMgr.shared.shouldAutoReloadUserDataFiles || forced { LMMgr.initUserLangModels() }
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
if #available(macOS 10.15, *) { FileObserveProject.shared.touch() }
if PrefMgr.shared.phraseEditorAutoReloadExternalModifications {
Broadcaster.shared.eventForReloadingPhraseEditor = .init()
}
@ -70,6 +69,8 @@ public extension AppDelegate {
SpeechSputnik.shared.refreshStatus() //
CandidateTextService.enableFinalSanityCheck()
// 使
// Debug
// Debug
@ -146,7 +147,7 @@ public extension AppDelegate {
guard let currentMemorySizeInBytes = NSApplication.memoryFootprint else { return 0 }
let currentMemorySize: Double = (Double(currentMemorySizeInBytes) / 1024 / 1024).rounded(toPlaces: 1)
switch currentMemorySize {
case 384...:
case 1024...:
vCLog("WARNING: EXCESSIVE MEMORY FOOTPRINT (\(currentMemorySize)MB).")
let msgPackage = UNMutableNotificationContent()
msgPackage.title = NSLocalizedString("vChewing", comment: "")
@ -168,4 +169,10 @@ public extension AppDelegate {
}
return currentMemorySize
}
// New About Window
@IBAction func about(_: Any) {
CtlAboutUI.show()
NSApp.popup()
}
}

View File

@ -0,0 +1,170 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import BrailleSputnik
import Foundation
import Shared
import Tekkon
public extension CandidateTextService {
// MARK: - Final Sanity Check Implementation.
static func enableFinalSanityCheck() {
finalSanityCheck = finalSanityCheckImplemented
}
private static func finalSanityCheckImplemented(_ target: CandidateTextService) -> Bool {
switch target.value {
case .url: return true
case let .selector(strSelector):
guard target.candidateText != "%s" else { return true } //
switch strSelector {
case "copyUnicodeMetadata:": return true
case _ where strSelector.hasPrefix("copyRuby"),
_ where strSelector.hasPrefix("copyBraille"),
_ where strSelector.hasPrefix("copyInline"):
return !target.reading.joined().isEmpty // 便 [""]
default: return true
}
}
}
// MARK: - Selector Methods, CandidatePairServicable, and the Coordinator.
var responseFromSelector: String? {
switch value {
case .url: return nil
case let .selector(string):
let passable = CandidatePairServicable(value: candidateText, reading: reading)
return Coordinator().runTask(selectorName: string, candidate: passable)
}
}
@objcMembers class CandidatePairServicable: NSObject {
public var value: String
public var reading: [String]
public init(value: String, reading: [String] = []) {
self.value = value
self.reading = reading
}
public typealias SubPair = (key: String, value: String)
@nonobjc var smashed: [SubPair] {
var pairs = [SubPair]()
if value.count != reading.count {
pairs.append((reading.joined(separator: " "), value))
} else {
value.enumerated().forEach { i, valChar in
pairs.append((reading[i], valChar.description))
}
}
return pairs
}
}
@objc class Coordinator: NSObject {
private var result: String?
public func runTask(selectorName: String, candidate param: CandidatePairServicable) -> String? {
guard !selectorName.isEmpty, !param.value.isEmpty else { return nil }
guard responds(to: Selector(selectorName)) else { return nil }
performSelector(onMainThread: Selector(selectorName), with: param, waitUntilDone: true)
defer { result = nil }
return result
}
/// Unicode
/// - Parameter param:
@objc func copyUnicodeMetadata(_ param: CandidatePairServicable) {
var resultArray = [String]()
param.value.forEach { char in
resultArray.append("\(char) \(char.description.charDescriptions.first ?? "NULL")")
}
result = resultArray.joined(separator: "\n")
}
/// HTML Ruby ()
/// - Parameter param:
@objc func copyRubyHTMLZhuyinTextbookStyle(_ param: CandidatePairServicable) {
prepareTextBookZhuyinReadings(param)
copyRubyHTMLCommon(param)
}
/// HTML Ruby ()
/// - Parameter param:
@objc func copyRubyHTMLHanyuPinyinTextbookStyle(_ param: CandidatePairServicable) {
prepareTextBookPinyinReadings(param)
copyRubyHTMLCommon(param)
}
/// ()
/// - Parameter param:
@objc func copyInlineZhuyinAnnotationTextbookStyle(_ param: CandidatePairServicable) {
prepareTextBookZhuyinReadings(param)
copyInlineAnnotationCommon(param)
}
/// ()
/// - Parameter param:
@objc func copyInlineHanyuPinyinAnnotationTextbookStyle(_ param: CandidatePairServicable) {
prepareTextBookPinyinReadings(param)
copyInlineAnnotationCommon(param)
}
@objc func copyBraille1947(_ param: CandidatePairServicable) {
result = BrailleSputnik(standard: .of1947).convertToBraille(smashedPairs: param.smashed)
}
@objc func copyBraille2018(_ param: CandidatePairServicable) {
result = BrailleSputnik(standard: .of2018).convertToBraille(smashedPairs: param.smashed)
}
// MARK: Privates
}
}
private extension CandidateTextService.Coordinator {
func copyInlineAnnotationCommon(_ param: CandidateTextService.CandidatePairServicable) {
var composed = ""
param.smashed.forEach { subPair in
let subKey = subPair.key
let subValue = subPair.value
composed += subKey.contains("_") ? subValue : "\(subValue)(\(subKey))"
}
result = composed
}
func copyRubyHTMLCommon(_ param: CandidateTextService.CandidatePairServicable) {
var composed = ""
param.smashed.forEach { subPair in
let subKey = subPair.key
let subValue = subPair.value
composed += subKey.contains("_") ? subValue : "<ruby>\(subValue)<rp>(</rp><rt>\(subKey)</rt><rp>)</rp></ruby>"
}
result = composed
}
func prepareTextBookZhuyinReadings(_ param: CandidateTextService.CandidatePairServicable) {
let newReadings = param.reading.map { currentReading in
if currentReading.contains("_") { return "_??" }
return Tekkon.cnvPhonaToTextbookStyle(target: currentReading)
}
param.reading = newReadings
}
func prepareTextBookPinyinReadings(_ param: CandidateTextService.CandidatePairServicable) {
let newReadings = param.reading.map { currentReading in
if currentReading.contains("_") { return "_??" }
return Tekkon.cnvHanyuPinyinToTextbookStyle(
targetJoined: Tekkon.cnvPhonaToHanyuPinyin(targetJoined: currentReading)
)
}
param.reading = newReadings
}
}

View File

@ -7,6 +7,7 @@
// requirements defined in MIT License.
import Hotenka
import Shared
public enum ChineseConverter {
public static let shared = HotenkaChineseConverter(

View File

@ -9,12 +9,12 @@
import AppKit
import Foundation
import Shared
import UniformTypeIdentifiers
public class VwrClientListMgr: NSViewController {
let windowWidth: CGFloat = 770
let contentWidth: CGFloat = 750
let buttonWidth: CGFloat = 150
let tableHeight: CGFloat = 230
lazy var tblClients: NSTableView = .init()
lazy var btnAddClient = NSButton("Add Client", target: self, action: #selector(btnAddClientClicked(_:)))
@ -35,7 +35,7 @@ public class VwrClientListMgr: NSViewController {
var body: NSView? {
NSStackView.build(.vertical, insets: .new(all: 14)) {
makeScrollableTable()
.makeSimpleConstraint(.height, relation: .equal, value: 232)
.makeSimpleConstraint(.height, relation: .equal, value: tableHeight)
NSStackView.build(.horizontal) {
let descriptionWidth = contentWidth - buttonWidth - 20
NSStackView.build(.vertical) {
@ -44,7 +44,7 @@ public class VwrClientListMgr: NSViewController {
.makeSimpleConstraint(.width, relation: .greaterThanOrEqual, value: descriptionWidth)
NSView()
}
NSStackView.build(.vertical) {
NSStackView.build(.vertical, spacing: 6) {
btnAddClient
.makeSimpleConstraint(.width, relation: .equal, value: buttonWidth)
btnRemoveClient
@ -59,6 +59,8 @@ public class VwrClientListMgr: NSViewController {
scrollContainer.scrollerStyle = .legacy
scrollContainer.autohidesScrollers = true
scrollContainer.documentView = tblClients
scrollContainer.hasVerticalScroller = true
scrollContainer.hasHorizontalScroller = true
if #available(macOS 11.0, *) {
tblClients.style = .inset
}
@ -72,12 +74,12 @@ public class VwrClientListMgr: NSViewController {
tblClients.autosaveTableColumns = false
tblClients.backgroundColor = NSColor.controlBackgroundColor
tblClients.columnAutoresizingStyle = .lastColumnOnlyAutoresizingStyle
tblClients.frame = CGRect(x: 0, y: 0, width: 728, height: 230)
tblClients.frame = CGRect(x: 0, y: 0, width: 728, height: tableHeight)
tblClients.gridColor = NSColor.clear
tblClients.intercellSpacing = CGSize(width: 17, height: 0)
tblClients.rowHeight = 24
tblClients.setContentHuggingPriority(.defaultHigh, for: .vertical)
tblClients.registerForDraggedTypes([.init(rawValue: kUTTypeFileURL as String)])
tblClients.registerForDraggedTypes([.kUTTypeFileURL])
tblClients.dataSource = self
tblClients.action = #selector(onItemClicked(_:))
tblClients.target = self
@ -150,7 +152,7 @@ extension VwrClientListMgr {
neta info: NSDraggingInfo, onError: @escaping () -> Void?, handler: (([URL]) -> Void)? = nil
) {
let board = info.draggingPasteboard
let type = NSPasteboard.PasteboardType(rawValue: kUTTypeApplicationBundle as String)
let type = NSPasteboard.PasteboardType.kUTTypeAppBundle
let options: [NSPasteboard.ReadingOptionKey: Any] = [
.urlReadingFileURLsOnly: true,
.urlReadingContentsConformToTypes: [type],
@ -196,7 +198,7 @@ extension VwrClientListMgr {
}
@IBAction func btnAddClientClicked(_: Any) {
guard let window = NSApp.keyWindow else { return }
guard let window = CtlClientListMgr.shared?.window else { return }
let alert = NSAlert()
alert.messageText = NSLocalizedString(
"Please enter the client app bundle identifier(s) you want to register.", comment: ""
@ -277,11 +279,11 @@ extension VwrClientListMgr {
)
let text = url.path + "\n\n" + NSLocalizedString("Please try again.", comment: "")
guard let bundle = Bundle(url: url) else {
NSApp.keyWindow?.callAlert(title: title, text: text)
CtlClientListMgr.shared?.window.callAlert(title: title, text: text)
return
}
guard let identifier = bundle.bundleIdentifier else {
NSApp.keyWindow?.callAlert(title: title, text: text)
CtlClientListMgr.shared?.window.callAlert(title: title, text: text)
return
}
let isIdentifierAlreadyRegistered = Self.clientsList.contains(identifier)

View File

@ -200,7 +200,8 @@ public extension IMEState {
case .ofCandidates where cursor != marker: return data.attributedStringMarking(for: session)
case .ofCandidates where cursor == marker: break
case .ofAssociates: return data.attributedStringPlaceholder(for: session)
case .ofSymbolTable where displayedText.isEmpty: return data.attributedStringPlaceholder(for: session)
case .ofSymbolTable where displayedText.isEmpty || node.containsCandidateServices:
return data.attributedStringPlaceholder(for: session)
case .ofSymbolTable where !displayedText.isEmpty: break
default: break
}

View File

@ -212,7 +212,7 @@ public extension IMEStateData {
subNeta = Tekkon.cnvPhonaToHanyuPinyin(targetJoined: subNeta)
subNeta = Tekkon.cnvHanyuPinyinToTextbookStyle(targetJoined: subNeta)
} else {
subNeta = Tekkon.cnvPhonaToTextbookReading(target: subNeta)
subNeta = Tekkon.cnvPhonaToTextbookStyle(target: subNeta)
}
}
arrOutput.append(subNeta)

View File

@ -19,8 +19,7 @@ import Tekkon
// MARK: - InputHandler (Protocol).
public protocol InputHandlerProtocol {
var currentLM: vChewingLM.LMInstantiator { get set }
var currentUOM: vChewingLM.LMUserOverride { get set }
var currentLM: LMAssembly.LMInstantiator { get set }
var delegate: InputHandlerDelegate? { get set }
var keySeparator: String { get }
static var keySeparator: String { get }
@ -99,8 +98,7 @@ public class InputHandler: InputHandlerProtocol {
var composer: Tekkon.Composer = .init() //
var compositor: Megrez.Compositor //
public var currentUOM: vChewingLM.LMUserOverride
public var currentLM: vChewingLM.LMInstantiator {
public var currentLM: LMAssembly.LMInstantiator {
didSet {
compositor.langModel = .init(withLM: currentLM)
clear()
@ -108,10 +106,9 @@ public class InputHandler: InputHandlerProtocol {
}
///
public init(lm: vChewingLM.LMInstantiator, uom: vChewingLM.LMUserOverride, pref: PrefMgrProtocol) {
public init(lm: LMAssembly.LMInstantiator, pref: PrefMgrProtocol) {
prefs = pref
currentLM = lm
currentUOM = uom
///
Megrez.Compositor.maxSpanLength = prefs.maxCandidateLength
/// ensureCompositor()
@ -369,8 +366,8 @@ public class InputHandler: InputHandlerProtocol {
let currentNode = currentWalk.findNode(at: actualNodeCursorPosition, target: &accumulatedCursor)
guard let currentNode = currentNode else { return }
uom: if currentNode.currentUnigram.score > -12, prefs.fetchSuggestionsFromUserOverrideModel {
if skipObservation { break uom }
uomProcessing: if currentNode.currentUnigram.score > -12, prefs.fetchSuggestionsFromUserOverrideModel {
if skipObservation { break uomProcessing }
vCLog("UOM: Start Observation.")
// 使
//
@ -378,9 +375,9 @@ public class InputHandler: InputHandlerProtocol {
prefs.failureFlagForUOMObservation = true
//
//
currentUOM.performObservation(
currentLM.performUOMObservation(
walkedBefore: previousWalk, walkedAfter: currentWalk, cursor: actualNodeCursorPosition,
timestamp: Date().timeIntervalSince1970, saveCallback: { self.currentUOM.saveData() }
timestamp: Date().timeIntervalSince1970
)
//
prefs.failureFlagForUOMObservation = false
@ -432,7 +429,7 @@ public class InputHandler: InputHandlerProtocol {
///
if !prefs.fetchSuggestionsFromUserOverrideModel { return arrResult }
///
let suggestion = currentUOM.fetchSuggestion(
let suggestion = currentLM.fetchUOMSuggestion(
currentWalk: compositor.walkedNodes, cursor: actualNodeCursorPosition, timestamp: Date().timeIntervalSince1970
)
arrResult.append(contentsOf: suggestion.candidates)

View File

@ -9,9 +9,9 @@
/// 調
import CandidateWindow
import CocoaExtension
import InputMethodKit
import Megrez
import OSFrameworkImpl
import Shared
// MARK: - § 調 (Handle Candidate State).
@ -30,6 +30,47 @@ extension InputHandler {
guard ctlCandidate.visible else { return false }
let inputText = ignoringModifiers ? (input.inputTextIgnoringModifiers ?? input.text) : input.text
let allowMovingCompositorCursor = state.type == .ofCandidates && !prefs.useSCPCTypingMode
let highlightedCandidate = state.candidates[ctlCandidate.highlightedIndex]
// MARK: Shift+?
var candidateTextServiceMenuRunning: Bool {
state.node.containsCandidateServices && state.type == .ofSymbolTable
}
serviceMenu: if prefs.useShiftQuestionToCallServiceMenu, input.commonKeyModifierFlags == .shift, input.text == "?" {
if candidateTextServiceMenuRunning { break serviceMenu }
let handled = handleServiceMenuInitiation(
candidateText: highlightedCandidate.value,
reading: highlightedCandidate.keyArray
)
if handled { return true }
}
// MARK: / /
if input.isSymbolMenuPhysicalKey {
switch input.commonKeyModifierFlags {
case .shift, [],
.option where !candidateTextServiceMenuRunning:
if !candidateTextServiceMenuRunning {
let handled = handleServiceMenuInitiation(
candidateText: highlightedCandidate.value,
reading: highlightedCandidate.keyArray
)
if handled { return true }
}
var updated = true
let reverseTrigger = input.isShiftHold || input.isOptionHold
updated = reverseTrigger ? ctlCandidate.showPreviousLine() : ctlCandidate.showNextLine()
if !updated { delegate.callError("66F3477B") }
return true
case .option where state.type == .ofSymbolTable:
//
return revolveTypingMethod(to: .haninKeyboardSymbol)
default: break
}
}
// MARK: 使
@ -101,7 +142,6 @@ extension InputHandler {
delegate.switchState(IMEState.ofAbortion())
return true
}
let highlightedCandidate = state.candidates[ctlCandidate.highlightedIndex] //
var handleAssociates = !prefs.useSCPCTypingMode && prefs.associatedPhrasesEnabled //
handleAssociates = handleAssociates && compositor.cursor == compositor.length //
confirmHighlightedCandidate()
@ -315,7 +355,7 @@ extension InputHandler {
}
}
// MARK: - Flipping pages by using modified bracket keys (when they are not occupied).
// MARK: Flipping pages by using modified bracket keys (when they are not occupied).
// Shift+Command+[] Chrome Ctrl
let ctrlCMD: Bool = input.commonKeyModifierFlags == [.control, .command]
@ -333,24 +373,6 @@ extension InputHandler {
}
}
// MARK: - Flipping pages by using symbol menu keys (when they are not occupied).
if input.isSymbolMenuPhysicalKey {
switch input.commonKeyModifierFlags {
case .shift, [],
.option where state.type != .ofSymbolTable:
var updated = true
let reverseTrigger = input.isShiftHold || input.isOptionHold
updated = reverseTrigger ? ctlCandidate.showPreviousLine() : ctlCandidate.showNextLine()
if !updated { delegate.callError("66F3477B") }
return true
case .option where state.type == .ofSymbolTable:
//
return revolveTypingMethod(to: .haninKeyboardSymbol)
default: break
}
}
if state.type == .ofInputting { return false } // `%quick`
delegate.callError("172A0F81")

View File

@ -58,7 +58,7 @@ private extension InputHandler {
func narrateTheComposer(with maybeKey: String? = nil, when condition: Bool, allowDuplicates: Bool = true) {
guard condition else { return }
let maybeKey = maybeKey ?? composer.phonabetKeyForQuery(pronouncable: prefs.acceptLeadingIntonations)
let maybeKey = maybeKey ?? composer.phonabetKeyForQuery(pronounceableOnly: prefs.acceptLeadingIntonations)
guard var keyToNarrate = maybeKey else { return }
if composer.intonation == Tekkon.Phonabet(" ") { keyToNarrate.append("ˉ") }
SpeechSputnik.shared.narrate(keyToNarrate, allowDuplicates: allowDuplicates)
@ -119,7 +119,7 @@ private extension InputHandler {
return handleEnter(input: input, readingOnly: true)
}
//
let maybeKey = composer.phonabetKeyForQuery(pronouncable: prefs.acceptLeadingIntonations)
let maybeKey = composer.phonabetKeyForQuery(pronounceableOnly: prefs.acceptLeadingIntonations)
guard let readingKey = maybeKey else { break ifComposeReading }
//
if !currentLM.hasUnigramsFor(keyArray: [readingKey]) {
@ -204,7 +204,7 @@ private extension InputHandler {
/// 調
if keyConsumedByReading {
// strict false
if composer.phonabetKeyForQuery(pronouncable: false) == nil {
if composer.phonabetKeyForQuery(pronounceableOnly: false) == nil {
// 調
if !composer.isPinyinMode, input.isSpace,
compositor.insertKey(existedIntonation.value)
@ -422,16 +422,9 @@ private extension InputHandler {
delegate.switchState(updatedState)
return true
}
let encoding: CFStringEncodings? = {
switch IMEApp.currentInputMode {
case .imeModeCHS: return .GB_18030_2000
case .imeModeCHT: return .big5_HKSCS_1999
default: return nil
}
}()
guard
var char = "\(strCodePointBuffer)\(input.text)"
.parsedAsHexLiteral(encoding: encoding)?.first?.description
.parsedAsHexLiteral(encoding: IMEApp.currentInputMode.nonUTFEncoding)?.first?.description
else {
delegate.callError("D220B880輸入的字碼沒有對應的字元。")
var updatedState = IMEState.ofAbortion()

View File

@ -393,75 +393,6 @@ extension InputHandler {
return true
}
// MARK: - Command+Enter
/// Command+Enter
/// - Parameter isShiftPressed: Shift
/// - Returns: SessionCtl IMK
private func commissionByCtrlCommandEnter(isShiftPressed: Bool = false) -> String {
var displayedText = compositor.keys.joined(separator: "\t")
if compositor.isEmpty {
displayedText = readingForDisplay
}
if !prefs.cassetteEnabled {
if prefs.inlineDumpPinyinInLieuOfZhuyin {
if !compositor.isEmpty {
var arrDisplayedTextElements = [String]()
compositor.keys.forEach { key in
arrDisplayedTextElements.append(Tekkon.restoreToneOneInPhona(target: key)) //
}
displayedText = arrDisplayedTextElements.joined(separator: "\t")
}
displayedText = Tekkon.cnvPhonaToHanyuPinyin(targetJoined: displayedText) //
}
if prefs.showHanyuPinyinInCompositionBuffer {
if compositor.isEmpty {
displayedText = displayedText.replacingOccurrences(of: "1", with: "")
}
}
}
displayedText = displayedText.replacingOccurrences(of: "\t", with: isShiftPressed ? "-" : " ")
return displayedText
}
// MARK: - Command+Option+Enter Ruby
/// Command+Option+Enter Ruby
/// - Parameter isShiftPressed: Shift
/// - Returns: SessionCtl IMK
private func commissionByCtrlOptionCommandEnter(isShiftPressed: Bool = false) -> String {
var composed = ""
compositor.walkedNodes.smashedPairs.forEach { key, value in
var key = key
if !prefs.cassetteEnabled {
key =
prefs.inlineDumpPinyinInLieuOfZhuyin
? Tekkon.restoreToneOneInPhona(target: key) //
: Tekkon.cnvPhonaToTextbookReading(target: key) //
if prefs.inlineDumpPinyinInLieuOfZhuyin {
key = Tekkon.cnvPhonaToHanyuPinyin(targetJoined: key) //
key = Tekkon.cnvHanyuPinyinToTextbookStyle(targetJoined: key) // 調
}
}
key = key.replacingOccurrences(of: "\t", with: " ")
if isShiftPressed {
if !composed.isEmpty { composed += " " }
composed += key.contains("_") ? "??" : key
return
}
//
composed += key.contains("_") ? value : "<ruby>\(value)<rp>(</rp><rt>\(key)</rt><rp>)</rp></ruby>"
}
return composed
}
// MARK: - BackSpace (macOS Delete)
/// BackSpace (macOS Delete)
@ -967,6 +898,20 @@ extension InputHandler {
}
}
// MARK: - (Service Menu)
func handleServiceMenuInitiation(candidateText: String, reading: [String]) -> Bool {
guard let delegate = delegate, delegate.state.type != .ofDeactivated else { return false }
guard !candidateText.isEmpty else { return false }
let rootNode = CandidateTextService.getCurrentServiceMenu(candidate: candidateText, reading: reading)
guard let rootNode = rootNode else { return false }
// commit buffer ESC
let textToCommit = generateStateOfInputting(sansReading: true).displayedText
delegate.switchState(IMEState.ofCommitting(textToCommit: textToCommit))
delegate.switchState(IMEState.ofSymbolTable(node: rootNode))
return true
}
// MARK: - Caps Lock Caps Lock and Alphanumerical mode
/// CapsLock
@ -1096,11 +1041,15 @@ extension InputHandler {
let fullWidthResult = behaviorValue % 2 != 0 //
triagePrefs: switch (behaviorValue, isConsideredEmptyForNow) {
case (2, _), (3, _), (4, false), (5, false):
currentLM.config.numPadFWHWStatus = fullWidthResult
currentLM.setOptions { config in
config.numPadFWHWStatus = fullWidthResult
}
if handlePunctuation("_NumPad_\(inputText)") { return true }
default: break triagePrefs // case 0 & 1
}
currentLM.config.numPadFWHWStatus = nil
currentLM.setOptions { config in
config.numPadFWHWStatus = nil
}
delegate.switchState(IMEState.ofEmpty())
let charToCommit = inputText.applyingTransformFW2HW(reverse: fullWidthResult)
delegate.switchState(IMEState.ofCommitting(textToCommit: charToCommit))

View File

@ -0,0 +1,125 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import BrailleSputnik
import Shared
import Tekkon
///
extension InputHandler {
// MARK: - (Shift+)Ctrl+Command+Enter
/// Command+Enter
/// - Parameter isShiftPressed: Shift
/// - Returns: SessionCtl IMK
func commissionByCtrlCommandEnter(isShiftPressed: Bool = false) -> String {
var displayedText = compositor.keys.joined(separator: "\t")
if compositor.isEmpty {
displayedText = readingForDisplay
}
if !prefs.cassetteEnabled {
if prefs.inlineDumpPinyinInLieuOfZhuyin {
if !compositor.isEmpty {
var arrDisplayedTextElements = [String]()
compositor.keys.forEach { key in
arrDisplayedTextElements.append(Tekkon.restoreToneOneInPhona(target: key)) //
}
displayedText = arrDisplayedTextElements.joined(separator: "\t")
}
displayedText = Tekkon.cnvPhonaToHanyuPinyin(targetJoined: displayedText) //
}
if prefs.showHanyuPinyinInCompositionBuffer {
if compositor.isEmpty {
displayedText = displayedText.replacingOccurrences(of: "1", with: "")
}
}
}
displayedText = displayedText.replacingOccurrences(of: "\t", with: isShiftPressed ? "-" : " ")
return displayedText
}
// MARK: - (Shift+)Ctrl+Command+Option+Enter Ruby
private enum CommitableMarkupType: Int {
case bareKeys = -1
case textWithBracketedAnnotations = 0
case textWithHTMLRubyAnnotations = 1
case braille1947 = 2
case braille2018 = 3
static func match(rawValue: Int) -> Self {
CommitableMarkupType(rawValue: rawValue) ?? .textWithBracketedAnnotations
}
var brailleStandard: BrailleSputnik.BrailleStandard? {
switch self {
case .braille1947: return .of1947
case .braille2018: return .of2018
default: return nil
}
}
}
/// Command+Option+Enter Ruby
///
/// prefs.specifyCmdOptCtrlEnterBehavior
/// 1.
/// 2. HTML Ruby
/// 3. (1947)
/// 4. (GF0019-2018)
/// - Parameter isShiftPressed: Shift
/// - Returns: SessionCtl IMK
func commissionByCtrlOptionCommandEnter(isShiftPressed: Bool = false) -> String {
var behavior = CommitableMarkupType.match(rawValue: prefs.specifyCmdOptCtrlEnterBehavior)
if prefs.cassetteEnabled, behavior.brailleStandard != nil {
behavior = .textWithBracketedAnnotations
}
if isShiftPressed { behavior = .bareKeys }
guard let brailleStandard = behavior.brailleStandard else {
return specifyTextMarkupToCommit(behavior: behavior)
}
let brailleProcessor = BrailleSputnik(standard: brailleStandard)
return brailleProcessor.convertToBraille(
smashedPairs: compositor.walkedNodes.smashedPairs,
extraInsertion: (reading: composer.value, cursor: compositor.cursor)
)
}
private func specifyTextMarkupToCommit(behavior: CommitableMarkupType) -> String {
var composed = ""
compositor.walkedNodes.smashedPairs.forEach { key, value in
var key = key
if !prefs.cassetteEnabled {
key =
prefs.inlineDumpPinyinInLieuOfZhuyin
? Tekkon.restoreToneOneInPhona(target: key) //
: Tekkon.cnvPhonaToTextbookStyle(target: key) //
if prefs.inlineDumpPinyinInLieuOfZhuyin {
key = Tekkon.cnvPhonaToHanyuPinyin(targetJoined: key) //
key = Tekkon.cnvHanyuPinyinToTextbookStyle(targetJoined: key) // 調
}
}
key = key.replacingOccurrences(of: "\t", with: " ")
switch behavior {
case .bareKeys:
if !composed.isEmpty { composed += " " }
composed += key.contains("_") ? "??" : key
case .textWithBracketedAnnotations:
composed += key.contains("_") ? value : "\(value)(\(key))"
case .textWithHTMLRubyAnnotations:
composed += key.contains("_") ? value : "<ruby>\(value)<rp>(</rp><rt>\(key)</rt><rp>)</rp></ruby>"
case .braille1947: break //
case .braille2018: break //
}
}
return composed
}
}

View File

@ -9,11 +9,11 @@
/// 調 IMK 調
/// 調
import CocoaExtension
import IMKUtils
import InputMethodKit
import LangModelAssembly
import Megrez
import OSFrameworkImpl
import Shared
// MARK: - § 調 (Handle Input with States) * Triage

View File

@ -7,6 +7,7 @@
// requirements defined in MIT License.
import Foundation
import Shared
import SwiftExtension
// MARK: - Typing Method
@ -30,12 +31,8 @@ public extension InputHandler {
case .codePoint:
let commonTerm = NSMutableString()
commonTerm.insert("Code Point Input.".localized, at: 0)
if !vertical {
switch IMEApp.currentInputMode {
case .imeModeCHS: commonTerm.insert("[GB] ", at: 0)
case .imeModeCHT: commonTerm.insert("[Big5] ", at: 0)
default: break
}
if !vertical, let initials = IMEApp.currentInputMode.nonUTFEncodingInitials {
commonTerm.insert("[\(initials)] ", at: 0)
}
return commonTerm.description
case .haninKeyboardSymbol:

View File

@ -15,26 +15,20 @@ import SwiftExtension
// MARK: - Input Mode Extension for Language Models
public extension Shared.InputMode {
private static let lmCHS = vChewingLM.LMInstantiator(isCHS: true)
private static let lmCHT = vChewingLM.LMInstantiator(isCHS: false)
private static let uomCHS = vChewingLM.LMUserOverride(dataURL: LMMgr.userOverrideModelDataURL(.imeModeCHS))
private static let uomCHT = vChewingLM.LMUserOverride(dataURL: LMMgr.userOverrideModelDataURL(.imeModeCHT))
private static let lmCHS = LMAssembly.LMInstantiator(
isCHS: true, uomDataURL: LMMgr.userOverrideModelDataURL(.imeModeCHS)
)
private static let lmCHT = LMAssembly.LMInstantiator(
isCHS: false, uomDataURL: LMMgr.userOverrideModelDataURL(.imeModeCHT)
)
var langModel: vChewingLM.LMInstantiator {
var langModel: LMAssembly.LMInstantiator {
switch self {
case .imeModeCHS: return Self.lmCHS
case .imeModeCHT: return Self.lmCHT
case .imeModeNULL: return .init()
}
}
var uom: vChewingLM.LMUserOverride {
switch self {
case .imeModeCHS: return Self.uomCHS
case .imeModeCHT: return Self.uomCHT
case .imeModeNULL: return .init(dataURL: LMMgr.userOverrideModelDataURL(IMEApp.currentInputMode))
}
}
}
// MARK: - Language Model Manager.
@ -54,14 +48,14 @@ public class LMMgr {
Self.loadUserPhrasesData()
}
public static var isCoreDBConnected: Bool { vChewingLM.LMInstantiator.isSQLDBConnected }
public static var isCoreDBConnected: Bool { LMAssembly.LMInstantiator.isSQLDBConnected }
public static func connectCoreDB(dbPath: String? = nil) {
guard let path: String = dbPath ?? Self.getCoreDictionaryDBPath() else {
assertionFailure("vChewing factory SQLite data not found.")
return
}
let result = vChewingLM.LMInstantiator.connectSQLDB(dbPath: path)
let result = LMAssembly.LMInstantiator.connectSQLDB(dbPath: path)
assert(result, "vChewing factory SQLite connection failed.")
Notifier.notify(
message: NSLocalizedString("Core Dict loading complete.", comment: "")
@ -71,10 +65,15 @@ public class LMMgr {
///
/// - Remark: cassettePath()
public static func loadCassetteData() {
vChewingLM.LMInstantiator.loadCassetteData(path: cassettePath())
func validateCassetteCandidateKey(_ target: String) -> Bool {
CandidateKey.validate(keys: target) == nil
}
LMAssembly.LMInstantiator.setCassetCandidateKeyValidator(validateCassetteCandidateKey)
LMAssembly.LMInstantiator.loadCassetteData(path: cassettePath())
}
public static func loadUserPhrasesData(type: vChewingLM.ReplacableUserDataType? = nil) {
public static func loadUserPhrasesData(type: LMAssembly.ReplacableUserDataType? = nil) {
guard let type = type else {
Shared.InputMode.validCases.forEach { mode in
mode.langModel.loadUserPhrasesData(
@ -82,12 +81,11 @@ public class LMMgr {
filterPath: userDictDataURL(mode: mode, type: .theFilter).path
)
mode.langModel.loadUserSymbolData(path: userDictDataURL(mode: mode, type: .theSymbols).path)
mode.uom.loadData(fromURL: userOverrideModelDataURL(mode))
mode.langModel.loadUOMData()
}
if PrefMgr.shared.associatedPhrasesEnabled { Self.loadUserAssociatesData() }
if PrefMgr.shared.phraseReplacementEnabled { Self.loadUserPhraseReplacement() }
if PrefMgr.shared.useSCPCTypingMode { Self.loadSCPCSequencesData() }
CandidateNode.load(url: Self.userSymbolMenuDataURL())
return
@ -131,12 +129,6 @@ public class LMMgr {
}
}
public static func loadSCPCSequencesData() {
Shared.InputMode.validCases.forEach { mode in
mode.langModel.loadSCPCSequencesData()
}
}
public static func reloadUserFilterDirectly(mode: Shared.InputMode) {
mode.langModel.reloadUserFilterDirectly(path: userDictDataURL(mode: mode, type: .theFilter).path)
}
@ -178,6 +170,7 @@ public class LMMgr {
config.isSymbolEnabled = PrefMgr.shared.symbolInputEnabled
config.isSCPCEnabled = PrefMgr.shared.useSCPCTypingMode
config.isCassetteEnabled = PrefMgr.shared.cassetteEnabled
config.filterNonCNSReadings = PrefMgr.shared.filterNonCNSReadingsForCHTInput
config.deltaOfCalendarYears = PrefMgr.shared.deltaOfCalendarYears
}
}
@ -186,12 +179,12 @@ public class LMMgr {
// MARK: UOM
public static func saveUserOverrideModelData() {
let globalQueue = DispatchQueue(label: "vChewingLM_UOM", qos: .unspecified, attributes: .concurrent)
let globalQueue = DispatchQueue(label: "LMAssembly_UOM", qos: .unspecified, attributes: .concurrent)
let group = DispatchGroup()
Shared.InputMode.validCases.forEach { mode in
group.enter()
globalQueue.async {
mode.uom.saveData(toURL: userOverrideModelDataURL(mode))
mode.langModel.saveUOMData()
group.leave()
}
}
@ -200,11 +193,11 @@ public class LMMgr {
}
public static func bleachSpecifiedSuggestions(targets: [String], mode: Shared.InputMode) {
mode.uom.bleachSpecifiedSuggestions(targets: targets, saveCallback: { mode.uom.saveData() })
mode.langModel.bleachSpecifiedUOMSuggestions(targets: targets)
}
public static func removeUnigramsFromUserOverrideModel(_ mode: Shared.InputMode) {
mode.uom.bleachUnigrams(saveCallback: { mode.uom.saveData() })
mode.langModel.bleachUOMUnigrams()
}
public static func relocateWreckedUOMData() {
@ -226,6 +219,6 @@ public class LMMgr {
}
public static func clearUserOverrideModelData(_ mode: Shared.InputMode = .imeModeNULL) {
mode.uom.clearData(withURL: userOverrideModelDataURL(mode))
mode.langModel.clearUOMData()
}
}

View File

@ -6,15 +6,78 @@
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import AppKit
import Foundation
import KimoDataReader
import LineReader
import Shared
public extension LMMgr {
enum KimoDataImportError: Error, LocalizedError {
case connectionFailure
case fileHandlerFailure
public var errorDescription: String? {
switch self {
case .fileHandlerFailure: return "i18n:KimoDataImportError.fileHandlerFailure.errMsg".localized
case .connectionFailure: return "i18n:KimoDataImportError.connectionFailure.errMsg".localized
}
}
}
/// XPC 使
/// - Parameter rawString: TXT
/// - Returns:
@discardableResult static func importYahooKeyKeyUserDictionaryByXPC() throws -> Int {
let kimoBundleID = "com.yahoo.inputmethod.KeyKey"
if #unavailable(macOS 11) {
NSWorkspace.shared.launchApplication(
withBundleIdentifier: kimoBundleID,
additionalEventParamDescriptor: nil,
launchIdentifier: nil
)
} else {
guard let imeURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: kimoBundleID) else {
throw KimoDataImportError.connectionFailure
}
NSWorkspace.shared.openApplication(at: imeURL, configuration: .init())
}
guard KimoCommunicator.shared.establishConnection() else { throw KimoDataImportError.connectionFailure }
var allPhrasesCHT = [UserPhrase]()
var allPhrasesCHS = [UserPhrase]()
KimoCommunicator.shared.prepareData { key, value in
let phraseCHT = UserPhrase(
keyArray: key.components(separatedBy: ","),
value: value,
inputMode: .imeModeCHT,
isConverted: false
)
guard phraseCHT.isValid, !phraseCHT.isDuplicated else { return }
guard !(phraseCHT.value.count == 1 && phraseCHT.keyArray.count == 1) else { return }
allPhrasesCHT.append(phraseCHT)
let phraseCHS = phraseCHT.crossConverted
guard phraseCHS.isValid, !phraseCHS.isDuplicated else { return }
guard !(phraseCHS.value.count == 1 && phraseCHS.keyArray.count == 1) else { return }
allPhrasesCHS.append(phraseCHS)
}
guard Self.batchImportUserPhrasePairs(allPhrasesCHT: allPhrasesCHT, allPhrasesCHS: allPhrasesCHS) else {
throw KimoDataImportError.fileHandlerFailure
}
let result = allPhrasesCHT.count
if result > 0 {
Broadcaster.shared.eventForReloadingPhraseEditor = .init()
}
return result
}
/// 使 TXT
/// - Parameter rawString: TXT
/// - Returns:
@discardableResult static func importYahooKeyKeyUserDictionary(text rawString: inout String) -> Int {
@discardableResult static func importYahooKeyKeyUserDictionary(text rawString: inout String) throws -> Int {
var allPhrasesCHT = [UserPhrase]()
var allPhrasesCHS = [UserPhrase]()
rawString.enumerateLines { currentLine, _ in
let cells = currentLine.split(separator: "\t")
guard cells.count >= 3, cells.first != "#", cells.first != "MJSR" else { return }
@ -24,12 +87,25 @@ public extension LMMgr {
guard phraseCHT.isValid, !phraseCHT.isDuplicated else { return }
guard !(phraseCHT.value.count == 1 && phraseCHT.keyArray.count == 1) else { return }
allPhrasesCHT.append(phraseCHT)
let phraseCHS = phraseCHT.crossConverted
guard phraseCHS.isValid, !phraseCHS.isDuplicated else { return }
guard !(phraseCHS.value.count == 1 && phraseCHS.keyArray.count == 1) else { return }
allPhrasesCHS.append(phraseCHS)
}
guard !allPhrasesCHT.isEmpty else { return 0 }
let allPhrasesCHS = allPhrasesCHT.compactMap { chtPhrase in
let chsPhrase = chtPhrase.crossConverted
return chsPhrase.isValid && !chsPhrase.isDuplicated ? chsPhrase : nil
}.deduplicated
guard Self.batchImportUserPhrasePairs(allPhrasesCHT: allPhrasesCHT, allPhrasesCHS: allPhrasesCHS) else {
throw KimoDataImportError.fileHandlerFailure
}
let result = allPhrasesCHT.count
if result > 0 {
Broadcaster.shared.eventForReloadingPhraseEditor = .init()
}
return result
}
private static func batchImportUserPhrasePairs(allPhrasesCHT: [UserPhrase], allPhrasesCHS: [UserPhrase]) -> Bool {
let outputStrCHS = allPhrasesCHS.map(\.description).joined(separator: "\n")
let outputStrCHT = allPhrasesCHT.map(\.description).joined(separator: "\n")
var outputDataCHS = "\(outputStrCHS)\n".data(using: .utf8) ?? .init([])
@ -39,7 +115,7 @@ public extension LMMgr {
let fileHandlerCHS = try? FileHandle(forUpdating: urlCHS)
let fileHandlerCHT = try? FileHandle(forUpdating: urlCHT)
guard let fileHandlerCHS = fileHandlerCHS, let fileHandlerCHT = fileHandlerCHT else { return 0 }
guard let fileHandlerCHS = fileHandlerCHS, let fileHandlerCHT = fileHandlerCHT else { return false }
defer {
fileHandlerCHS.closeFile()
fileHandlerCHT.closeFile()
@ -62,8 +138,7 @@ public extension LMMgr {
}
fileHandlerCHT.seekToEndOfFile()
fileHandlerCHT.write(outputDataCHT)
return allPhrasesCHT.count
return true
}
}

View File

@ -17,23 +17,23 @@ import Shared
extension LMMgr: PhraseEditorDelegate {
public var currentInputMode: Shared.InputMode { IMEApp.currentInputMode }
public func openPhraseFile(mode: Shared.InputMode, type: vChewingLM.ReplacableUserDataType, using app: FileOpenMethod) {
public func openPhraseFile(mode: Shared.InputMode, type: LMAssembly.ReplacableUserDataType, using app: FileOpenMethod) {
Self.openPhraseFile(fromURL: Self.userDictDataURL(mode: mode, type: type), using: app)
}
public func consolidate(text strProcessed: inout String, pragma shouldCheckPragma: Bool) {
vChewingLM.LMConsolidator.consolidate(text: &strProcessed, pragma: shouldCheckPragma)
LMAssembly.LMConsolidator.consolidate(text: &strProcessed, pragma: shouldCheckPragma)
}
public func checkIfPhrasePairExists(userPhrase: String, mode: Shared.InputMode, key unigramKey: String) -> Bool {
Self.checkIfPhrasePairExists(userPhrase: userPhrase, mode: mode, keyArray: [unigramKey])
}
public func retrieveData(mode: Shared.InputMode, type: vChewingLM.ReplacableUserDataType) -> String {
public func retrieveData(mode: Shared.InputMode, type: LMAssembly.ReplacableUserDataType) -> String {
Self.retrieveData(mode: mode, type: type)
}
public static func retrieveData(mode: Shared.InputMode, type: vChewingLM.ReplacableUserDataType) -> String {
public static func retrieveData(mode: Shared.InputMode, type: LMAssembly.ReplacableUserDataType) -> String {
vCLog("Retrieving data. Mode: \(mode.localizedDescription), type: \(type.localizedDescription)")
let theURL = Self.userDictDataURL(mode: mode, type: type)
do {
@ -44,12 +44,12 @@ extension LMMgr: PhraseEditorDelegate {
}
}
public func saveData(mode: Shared.InputMode, type: vChewingLM.ReplacableUserDataType, data: String) -> String {
public func saveData(mode: Shared.InputMode, type: LMAssembly.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
mode: Shared.InputMode, type: LMAssembly.ReplacableUserDataType, data: String
) -> String {
DispatchQueue.main.async {
let theURL = Self.userDictDataURL(mode: mode, type: type)

View File

@ -6,8 +6,10 @@
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import CandidateWindow
import Foundation
import LangModelAssembly
import Megrez
import Shared
// MARK: - 使
@ -32,6 +34,10 @@ public extension LMMgr {
!keyArray.isEmpty && keyArray.filter(\.isEmpty).isEmpty && !value.isEmpty
}
var isSingleCharReadingPair: Bool {
value.count == 1 && keyArray.count == 1 && keyArray.first?.first != "_"
}
public var description: String {
descriptionCells.joined(separator: " ")
}
@ -88,7 +94,7 @@ public extension LMMgr {
/// 使
///
///
let theType: vChewingLM.ReplacableUserDataType = toFilter ? .theFilter : .thePhrases
let theType: LMAssembly.ReplacableUserDataType = toFilter ? .theFilter : .thePhrases
let theURL = LMMgr.userDictDataURL(mode: inputMode, type: theType)
var fileSize: UInt64?
do {
@ -143,7 +149,7 @@ public extension LMMgr {
}
}
let theURL = LMMgr.userDictDataURL(mode: inputMode, type: .theFilter)
if forceConsolidate, !vChewingLM.LMConsolidator.consolidate(path: theURL.path, pragma: false) { return false }
if forceConsolidate, !LMAssembly.LMConsolidator.consolidate(path: theURL.path, pragma: false) { return false }
// Get FileSize.
var fileSize: UInt64?
do {
@ -190,3 +196,51 @@ public extension LMMgr {
}
}
}
// MARK: - Weight Suggestions.
public extension LMMgr.UserPhrase {
mutating func updateWeight(basedOn action: CandidateContextMenuAction) {
weight = suggestNextFreq(for: action)
}
func suggestNextFreq(for action: CandidateContextMenuAction, extreme: Bool = false) -> Double? {
var extremeFallbackResult: Double? {
switch action {
case .toBoost: return nil // 0
case .toNerf: return -114.514
case .toFilter: return nil
}
}
guard !extreme, isSingleCharReadingPair else { return extremeFallbackResult }
let fetchedUnigrams = inputMode.langModel.unigramsFor(keyArray: keyArray)
let currentWeight = weight ?? fetchedUnigrams.first { $0.value == value }?.score
guard let currentWeight = currentWeight else { return extremeFallbackResult }
let fetchedScores = fetchedUnigrams.map(\.score)
var neighborValue: Double?
switch action {
case .toBoost:
neighborValue = currentWeight.findNeighborValue(from: fetchedScores, greater: true)
if let realNeighborValue = neighborValue {
neighborValue = realNeighborValue + 0.000_001
} else if let fetchedMax = fetchedScores.min(), currentWeight <= fetchedMax {
neighborValue = Swift.min(0, currentWeight + 0.000_001)
} else {
//
neighborValue = Swift.min(0, currentWeight + 1)
}
case .toNerf:
neighborValue = currentWeight.findNeighborValue(from: fetchedScores, greater: false)
if let realNeighborValue = neighborValue {
neighborValue = realNeighborValue - 0.000_001
} else if let fetchedMax = fetchedScores.max(), currentWeight >= fetchedMax {
neighborValue = Swift.max(-114.514, currentWeight - 0.000_001)
} else {
//
neighborValue = Swift.max(-114.514, currentWeight - 1)
}
case .toFilter: return nil
}
return neighborValue ?? extremeFallbackResult
}
}

View File

@ -61,7 +61,7 @@ public extension LMMgr {
/// - mode:
/// - type:
/// - Returns: URL
static func userDictDataURL(mode: Shared.InputMode, type: vChewingLM.ReplacableUserDataType) -> URL {
static func userDictDataURL(mode: Shared.InputMode, type: LMAssembly.ReplacableUserDataType) -> URL {
var fileName: String = {
switch type {
case .thePhrases: return "userdata"
@ -240,7 +240,6 @@ public extension LMMgr {
}
// 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 {
Broadcaster.shared.eventForReloadingPhraseEditor = .init()
}
@ -272,7 +271,7 @@ public extension LMMgr {
return true
}
static func openUserDictFile(type: vChewingLM.ReplacableUserDataType, dual: Bool = false, alt: Bool) {
static func openUserDictFile(type: LMAssembly.ReplacableUserDataType, dual: Bool = false, alt: Bool) {
let app: FileOpenMethod = alt ? .textEdit : .finder
openPhraseFile(fromURL: userDictDataURL(mode: IMEApp.currentInputMode, type: type), using: app)
guard dual else { return }
@ -325,7 +324,7 @@ public extension LMMgr {
///
///
var failed = false
caseCheck: for type in vChewingLM.ReplacableUserDataType.allCases {
caseCheck: for type in LMAssembly.ReplacableUserDataType.allCases {
let templateName = Self.templateName(for: type, mode: mode)
if !ensureFileExists(userDictDataURL(mode: mode, type: type), deployTemplate: templateName) {
failed = true
@ -335,7 +334,7 @@ public extension LMMgr {
return !failed
}
internal static func templateName(for type: vChewingLM.ReplacableUserDataType, mode: Shared.InputMode) -> String {
internal static func templateName(for type: LMAssembly.ReplacableUserDataType, mode: Shared.InputMode) -> String {
switch type {
case .thePhrases: return kTemplateNameUserPhrases
case .theFilter: return kTemplateNameUserFilterList
@ -346,3 +345,26 @@ public extension LMMgr {
}
}
}
// MARK: - 使 JSON
// JSON 便
public extension LMMgr {
@discardableResult static func dumpUserDictDataToJSON(print: Bool = false, all: Bool) -> String? {
var summarizedDict = [LMAssembly.UserDictionarySummarized]()
Shared.InputMode.allCases.forEach { mode in
guard mode != .imeModeNULL else { return }
summarizedDict.append(mode.langModel.summarize(all: all))
}
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
if #available(macOS 10.13, *) {
encoder.outputFormatting.insert(.sortedKeys)
}
guard let data = try? encoder.encode(summarizedDict) else { return nil }
guard let outputStr = String(data: data, encoding: .utf8) else { return nil }
if print { Swift.print(outputStr) }
return outputStr
}
}

View File

@ -0,0 +1,36 @@
// (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 Shared
public extension PrefMgr {
static let shared: PrefMgr = {
let result = PrefMgr()
result.assignDidSetActions()
return result
}()
private func assignDidSetActions() {
didAskForSyncingLMPrefs = {
if PrefMgr.shared.phraseReplacementEnabled {
LMMgr.loadUserPhraseReplacement()
}
if PrefMgr.shared.associatedPhrasesEnabled {
LMMgr.loadUserAssociatesData()
}
LMMgr.syncLMPrefs()
}
didAskForRefreshingSpeechSputnik = {
SpeechSputnik.shared.refreshStatus()
}
didAskForSyncingShiftKeyDetectorPrefs = {
SessionCtl.theShiftKeyDetector.toggleWithLShift = PrefMgr.shared.togglingAlphanumericalModeWithLShift
SessionCtl.theShiftKeyDetector.toggleWithRShift = PrefMgr.shared.togglingAlphanumericalModeWithRShift
}
}
}

View File

@ -150,7 +150,7 @@ class FrmRevLookupWindow: NSWindow {
strBuilder.append("Maximum 15 results returnable.".localized + "\n")
break theLoop
}
let arrResult = vChewingLM.LMInstantiator.getFactoryReverseLookupData(with: char)?.deduplicated ?? []
let arrResult = LMAssembly.LMInstantiator.getFactoryReverseLookupData(with: char)?.deduplicated ?? []
if !arrResult.isEmpty {
strBuilder.append(char + "\t")
strBuilder.append(arrResult.joined(separator: ", "))

View File

@ -8,7 +8,7 @@
import AppKit
import Carbon
import CocoaExtension
import OSFrameworkImpl
import Shared
public class SecurityAgentHelper {

View File

@ -0,0 +1,63 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import AppKit
import Foundation
public class CtlServiceMenuEditor: NSWindowController {
let viewController = VwrServiceMenuEditor()
public static var shared: CtlServiceMenuEditor?
public init() {
super.init(
window: .init(
contentRect: CGRect(x: 401, y: 295, width: 770, height: 335),
styleMask: [.titled, .closable, .miniaturizable],
backing: .buffered,
defer: true
)
)
viewController.windowController = self
viewController.loadView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
public static func show() {
if shared == nil {
shared = CtlServiceMenuEditor()
}
guard let shared = shared, let sharedWindow = shared.window else { return }
if !sharedWindow.isVisible {
shared.windowDidLoad()
}
sharedWindow.setPosition(vertical: .center, horizontal: .right, padding: 20)
sharedWindow.orderFrontRegardless() //
sharedWindow.title = "Service Menu Editor".localized
sharedWindow.level = .statusBar
if #available(macOS 10.10, *) {
sharedWindow.titlebarAppearsTransparent = true
}
shared.showWindow(shared)
NSApp.popup()
}
override public func windowDidLoad() {
super.windowDidLoad()
let view = viewController.view
window?.contentView = view
if let window = window {
var frame = window.frame
frame.size = view.fittingSize
window.setFrame(frame, display: true)
}
window?.setPosition(vertical: .center, horizontal: .right, padding: 20)
}
}

View File

@ -0,0 +1,368 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import AppKit
import Foundation
import OSFrameworkImpl
import Shared
public class VwrServiceMenuEditor: NSViewController {
let windowWidth: CGFloat = 770
let contentWidth: CGFloat = 750
let tableHeight: CGFloat = 230
lazy var tblServices: NSTableView = .init()
lazy var btnShowInstructions = NSButton("How to Fill", target: self, action: #selector(btnShowInstructionsClicked(_:)))
lazy var btnAddService = NSFileDragRetrieverButton(
"Add Service",
target: self,
action: #selector(btnAddServiceClicked(_:)),
postDrag: handleDrag
)
lazy var btnRemoveService = NSButton("Remove Selected", target: self, action: #selector(btnRemoveServiceClicked(_:)))
lazy var btnResetService = NSButton("Reset Default", target: self, action: #selector(btnResetServiceClicked(_:)))
lazy var btnCopyAllToClipboard = NSButton("Copy All to Clipboard", target: self, action: #selector(btnCopyAllToClipboardClicked(_:)))
lazy var tableColumn1Cell = NSTextFieldCell()
lazy var tableColumn1 = NSTableColumn()
lazy var tableColumn2Cell = NSTextFieldCell()
lazy var tableColumn2 = NSTableColumn()
var windowController: NSWindowController?
public convenience init(windowController: NSWindowController? = nil) {
self.init()
self.windowController = windowController
}
override public func loadView() {
tblServices.reloadData()
view = body ?? .init()
(view as? NSStackView)?.alignment = .centerX
view.makeSimpleConstraint(.width, relation: .equal, value: windowWidth)
btnRemoveService.keyEquivalent = .init(NSEvent.SpecialKey.delete.unicodeScalar)
}
var body: NSView? {
NSStackView.build(.vertical, insets: .new(all: 14)) {
NSStackView.build(.horizontal) {
btnAddService
btnRemoveService
btnCopyAllToClipboard
btnShowInstructions
NSView()
btnResetService
}
makeScrollableTable()
.makeSimpleConstraint(.height, relation: .equal, value: tableHeight)
NSStackView.build(.horizontal) {
let descriptionWidth = contentWidth - 10
NSStackView.build(.vertical) {
let strDescription = "i18n:CandidateServiceMenuEditor.description"
strDescription.makeNSLabel(descriptive: true, fixWidth: descriptionWidth)
.makeSimpleConstraint(.width, relation: .greaterThanOrEqual, value: descriptionWidth)
NSView()
}
}
}
}
func makeScrollableTable() -> NSScrollView {
let scrollContainer = NSScrollView()
scrollContainer.scrollerStyle = .legacy
scrollContainer.autohidesScrollers = true
scrollContainer.documentView = tblServices
scrollContainer.hasVerticalScroller = true
scrollContainer.hasHorizontalScroller = true
if #available(macOS 11.0, *) {
tblServices.style = .inset
}
tblServices.addTableColumn(tableColumn1)
tblServices.addTableColumn(tableColumn2)
// tblServices.headerView = nil
tblServices.delegate = self
tblServices.allowsExpansionToolTips = true
tblServices.allowsMultipleSelection = true
tblServices.autoresizingMask = [.width, .height]
tblServices.autosaveTableColumns = false
tblServices.backgroundColor = NSColor.controlBackgroundColor
tblServices.columnAutoresizingStyle = .lastColumnOnlyAutoresizingStyle
tblServices.frame = CGRect(x: 0, y: 0, width: 728, height: tableHeight)
tblServices.gridColor = NSColor.clear
tblServices.intercellSpacing = CGSize(width: 15, height: 0)
tblServices.setContentHuggingPriority(.defaultHigh, for: .vertical)
tblServices.registerForDraggedTypes([.kUTTypeData, .kUTTypeFileURL])
tblServices.dataSource = self
tblServices.target = self
if #available(macOS 11.0, *) { tblServices.style = .inset }
tableColumn1.identifier = NSUserInterfaceItemIdentifier("colTitle")
tableColumn1.headerCell.title = "i18n:CandidateServiceMenuEditor.table.field.MenuTitle".localized
tableColumn1.maxWidth = 280
tableColumn1.minWidth = 200
tableColumn1.resizingMask = [.autoresizingMask, .userResizingMask]
tableColumn1.width = 200
tableColumn1.dataCell = tableColumn1Cell
tableColumn1Cell.font = NSFont.systemFont(ofSize: 13)
tableColumn1Cell.isEditable = true
tableColumn1Cell.isSelectable = true
tableColumn1Cell.lineBreakMode = .byTruncatingTail
tableColumn1Cell.stringValue = "Text Cell"
tableColumn1Cell.textColor = NSColor.controlTextColor
tableColumn2.identifier = NSUserInterfaceItemIdentifier("colValue")
tableColumn2.headerCell.title = "i18n:CandidateServiceMenuEditor.table.field.Value".localized
tableColumn2.maxWidth = 1000
tableColumn2.minWidth = 40
tableColumn2.resizingMask = [.autoresizingMask, .userResizingMask]
tableColumn2.width = 480
tableColumn2.dataCell = tableColumn2Cell
tableColumn2Cell.backgroundColor = NSColor.controlBackgroundColor
tableColumn2Cell.font = NSFont.systemFont(ofSize: 13)
tableColumn2Cell.isEditable = true
tableColumn2Cell.isSelectable = true
tableColumn2Cell.lineBreakMode = .byTruncatingTail
tableColumn2Cell.stringValue = "Text Cell"
tableColumn2Cell.textColor = NSColor.controlTextColor
return scrollContainer
}
}
// MARK: - UserDefaults Handlers.
public extension VwrServiceMenuEditor {
static var servicesList: [CandidateTextService] {
get {
PrefMgr.shared.candidateServiceMenuContents.parseIntoCandidateTextServiceStack()
}
set {
PrefMgr.shared.candidateServiceMenuContents = newValue.rawRepresentation
}
}
static func removeService(at index: Int) {
guard index < Self.servicesList.count else { return }
Self.servicesList.remove(at: index)
}
}
// MARK: - Common Operation Methods.
extension VwrServiceMenuEditor {
func refresh() {
tblServices.reloadData()
reassureButtonAvailability()
}
func reassureButtonAvailability() {
btnRemoveService.isEnabled = (0 ..< Self.servicesList.count).contains(
tblServices.selectedRow)
}
func handleDrag(_ givenURL: URL) {
guard let string = try? String(contentsOf: givenURL) else { return }
Self.servicesList.append(contentsOf: string.components(separatedBy: .newlines).parseIntoCandidateTextServiceStack())
refresh()
}
}
// MARK: - IBActions.
extension VwrServiceMenuEditor {
@IBAction func btnShowInstructionsClicked(_: Any) {
let strTitle = "How to Fill".localized
let strFillGuide = "i18n:CandidateServiceMenuEditor.formatGuide".localized
windowController?.window.callAlert(title: strTitle, text: strFillGuide)
}
@IBAction func btnResetServiceClicked(_: Any) {
PrefMgr.shared.candidateServiceMenuContents = PrefMgr.kDefaultCandidateServiceMenuItem
tblServices.reloadData()
}
@IBAction func btnCopyAllToClipboardClicked(_: Any) {
var resultArrayLines = [String]()
Self.servicesList.forEach { currentService in
resultArrayLines.append("\(currentService.key)\t\(currentService.definedValue)")
}
let result = resultArrayLines.joined(separator: "\n").appending("\n")
NSPasteboard.general.declareTypes([.string], owner: nil)
NSPasteboard.general.setString(result, forType: .string)
}
@IBAction func btnRemoveServiceClicked(_: Any) {
guard let minIndexSelected = tblServices.selectedRowIndexes.min() else { return }
if minIndexSelected >= Self.servicesList.count { return }
if minIndexSelected < 0 { return }
var isLastRow = false
tblServices.selectedRowIndexes.sorted().reversed().forEach { index in
isLastRow = {
if Self.servicesList.count < 2 { return false }
return minIndexSelected == Self.servicesList.count - 1
}()
if index < Self.servicesList.count {
Self.removeService(at: index)
}
}
if isLastRow {
tblServices.selectRowIndexes(.init(arrayLiteral: minIndexSelected - 1), byExtendingSelection: false)
}
tblServices.reloadData()
btnRemoveService.isEnabled = (0 ..< Self.servicesList.count).contains(minIndexSelected)
}
@IBAction func btnAddServiceClicked(_: Any) {
guard let window = windowController?.window else { return }
let alert = NSAlert()
alert.messageText = NSLocalizedString(
"i18n:CandidateServiceMenuEditor.prompt", comment: ""
)
alert.informativeText = NSLocalizedString(
"i18n:CandidateServiceMenuEditor.howToGetGuide", comment: ""
)
alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
let maxFloat = Double(Float.greatestFiniteMagnitude)
let scrollview = NSScrollView(frame: NSRect(x: 0, y: 0, width: 512, height: 200))
let contentSize = scrollview.contentSize
scrollview.borderType = .noBorder
scrollview.hasVerticalScroller = true
scrollview.hasHorizontalScroller = true
scrollview.horizontalScroller?.scrollerStyle = .legacy
scrollview.verticalScroller?.scrollerStyle = .legacy
scrollview.autoresizingMask = [.width, .height]
let theTextView = NSTextView(frame: NSRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height))
scrollview.documentView = theTextView
theTextView.minSize = NSSize(width: 0.0, height: contentSize.height)
theTextView.maxSize = NSSize(width: maxFloat, height: maxFloat)
theTextView.isVerticallyResizable = true
theTextView.isHorizontallyResizable = false
theTextView.autoresizingMask = .width
theTextView.textContainer?.containerSize = NSSize(width: contentSize.width, height: maxFloat)
theTextView.textContainer?.widthTracksTextView = true
theTextView.enclosingScrollView?.hasHorizontalScroller = true
theTextView.isHorizontallyResizable = true
theTextView.autoresizingMask = [.width, .height]
theTextView.textContainer?.containerSize = NSSize(width: maxFloat, height: maxFloat)
theTextView.textContainer?.widthTracksTextView = false
theTextView.toolTip = "i18n:CandidateServiceMenuEditor.formatGuide".localized
alert.accessoryView = scrollview
alert.beginSheetModal(at: window) { result in
switch result {
case .alertFirstButtonReturn:
let rawLines = theTextView.textContainer?.textView?.string.components(separatedBy: .newlines) ?? []
self.tblServices.beginUpdates()
Self.servicesList.append(contentsOf: rawLines.parseIntoCandidateTextServiceStack())
self.tblServices.endUpdates()
default: return
}
}
}
}
// MARK: - TableView Extensions.
extension VwrServiceMenuEditor: NSTableViewDelegate, NSTableViewDataSource {
public func numberOfRows(in _: NSTableView) -> Int {
Self.servicesList.count
}
public func tableView(_: NSTableView, shouldEdit _: NSTableColumn?, row _: Int) -> Bool {
false
}
public func tableView(_: NSTableView, objectValueFor column: NSTableColumn?, row: Int) -> Any? {
defer {
self.btnRemoveService.isEnabled = (0 ..< Self.servicesList.count).contains(
self.tblServices.selectedRow)
}
guard row < Self.servicesList.count else { return "" }
if let column = column {
let colName = column.identifier.rawValue
switch colName {
case "colTitle": return Self.servicesList[row].key
case "colValue": return Self.servicesList[row].definedValue // TODO:
default: return ""
}
}
return Self.servicesList[row]
}
// MARK: Pasteboard Operations.
public func tableView(
_: NSTableView, pasteboardWriterForRow row: Int
) -> NSPasteboardWriting? {
let pasteboard = NSPasteboardItem()
pasteboard.setString(row.description, forType: .string)
return pasteboard
}
public func tableView(
_: NSTableView,
validateDrop _: NSDraggingInfo,
proposedRow _: Int,
proposedDropOperation _: NSTableView.DropOperation
) -> NSDragOperation {
.move
}
public func tableView(
_ tableView: NSTableView,
acceptDrop info: NSDraggingInfo,
row: Int,
dropOperation _: NSTableView.DropOperation
) -> Bool {
var oldIndexes = [Int]()
info.enumerateDraggingItems(
options: [],
for: tableView,
classes: [NSPasteboardItem.self],
searchOptions: [:]
) { dragItem, _, _ in
guard let pasteboardItem = dragItem.item as? NSPasteboardItem else { return }
guard let index = Int(pasteboardItem.string(forType: .string) ?? "NULL"), index >= 0 else { return }
oldIndexes.append(index)
}
var oldIndexOffset = 0
var newIndexOffset = 0
tableView.beginUpdates()
for oldIndex in oldIndexes {
if oldIndex < row {
let contentToMove = Self.servicesList[oldIndex + oldIndexOffset]
Self.servicesList.remove(at: oldIndex + oldIndexOffset)
Self.servicesList.insert(contentToMove, at: row - 1)
tableView.moveRow(at: oldIndex + oldIndexOffset, to: row - 1)
oldIndexOffset -= 1
} else {
let contentToMove = Self.servicesList[oldIndex]
Self.servicesList.remove(at: oldIndex)
Self.servicesList.insert(contentToMove, at: row + newIndexOffset)
tableView.moveRow(at: oldIndex, to: row + newIndexOffset)
newIndexOffset += 1
}
}
tableView.endUpdates()
reassureButtonAvailability()
return true
}
}
// MARK: - Preview.
@available(macOS 14.0, *)
#Preview(traits: .fixedLayout(width: 770, height: 335)) {
VwrServiceMenuEditor()
}

View File

@ -7,10 +7,10 @@
// requirements defined in MIT License.
import CandidateWindow
import CocoaExtension
import IMKUtils
import InputMethodKit
import NotifierUI
import OSFrameworkImpl
import PopupCompositionBuffer
import Shared
import ShiftKeyUpChecker
@ -178,7 +178,6 @@ public class SessionCtl: IMKInputController {
// ----------------------------
///
inputHandler?.currentLM = inputMode.langModel //
inputHandler?.currentUOM = inputMode.uom
///
inputHandler?.ensureKeyboardParser()
///
@ -214,9 +213,7 @@ public class SessionCtl: IMKInputController {
//
Self.current?.hidePalettes()
Self.current = self
self.inputHandler = InputHandler(
lm: self.inputMode.langModel, uom: self.inputMode.uom, pref: PrefMgr.shared
)
self.inputHandler = InputHandler(lm: self.inputMode.langModel, pref: PrefMgr.shared)
self.inputHandler?.delegate = self
self.syncBaseLMPrefs()
//
@ -313,9 +310,7 @@ public extension SessionCtl {
if self.isActivated { return }
// setValue() IMK activateServer() setValue()
self.inputHandler = InputHandler(
lm: self.inputMode.langModel, uom: self.inputMode.uom, pref: PrefMgr.shared
)
self.inputHandler = InputHandler(lm: self.inputMode.langModel, pref: PrefMgr.shared)
self.inputHandler?.delegate = self
self.syncBaseLMPrefs()

View File

@ -46,8 +46,11 @@ extension SessionCtl: InputHandlerDelegate {
var userPhrase = LMMgr.UserPhrase(
keyArray: kvPair.keyArray, value: kvPair.value, inputMode: inputMode
)
if Self.areWeNerfing { userPhrase.weight = -114.514 }
LMMgr.writeUserPhrasesAtOnce(userPhrase, areWeFiltering: addToFilter) {
var action = CandidateContextMenuAction.toBoost
if Self.areWeNerfing { action = .toNerf }
if addToFilter { action = .toFilter }
userPhrase.updateWeight(basedOn: action)
LMMgr.writeUserPhrasesAtOnce(userPhrase, areWeFiltering: action == .toFilter) {
succeeded = false
}
if !succeeded { return false }
@ -80,6 +83,7 @@ extension SessionCtl: InputHandlerDelegate {
extension SessionCtl: CtlCandidateDelegate {
public var isCandidateState: Bool { state.isCandidateContainer }
public var showCodePointForCurrentCandidate: Bool { PrefMgr.shared.showCodePointInCandidateUI }
public var clientAccentColor: NSColor? {
var nullResponse = !PrefMgr.shared.respectClientAccentColor
@ -124,6 +128,8 @@ extension SessionCtl: CtlCandidateDelegate {
return shortened ? theEmoji : "\(theEmoji) " + NSLocalizedString("Quick Candidates", comment: "")
} else if PrefMgr.shared.cassetteEnabled {
return shortened ? "📼" : "📼 " + NSLocalizedString("CIN Cassette Mode", comment: "")
} else if state.type == .ofSymbolTable, state.node.containsCandidateServices {
return shortened ? "🌎" : "🌎 " + NSLocalizedString("Service Menu", comment: "")
}
return ""
}
@ -200,6 +206,22 @@ extension SessionCtl: CtlCandidateDelegate {
let node = state.node.members[index]
if !node.members.isEmpty {
switchState(IMEState.ofSymbolTable(node: node))
} else if let serviceNode = node.asServiceMenuNode {
switch serviceNode.service.value {
case let .url(theURL):
// Safari 使
NSWorkspace.shared.open(theURL)
case .selector:
if let response = serviceNode.service.responseFromSelector {
NSPasteboard.general.declareTypes([.string], owner: nil)
NSPasteboard.general.setString(response, forType: .string)
Notifier.notify(message: "i18n:candidateServiceMenu.selectorResponse.succeeded".localized)
} else {
callError("4DFDC487: Candidate Text Service Selector Responsiveness Failure.")
Notifier.notify(message: "i18n:candidateServiceMenu.selectorResponse.failed".localized)
}
}
switchState(IMEState.ofAbortion())
} else {
switchState(IMEState.ofCommitting(textToCommit: node.name))
}
@ -256,7 +278,7 @@ extension SessionCtl: CtlCandidateDelegate {
var userPhrase = LMMgr.UserPhrase(
keyArray: rawPair.keyArray, value: rawPair.value, inputMode: inputMode
)
if action == .toNerf { userPhrase.weight = -114.514 }
userPhrase.updateWeight(basedOn: action)
LMMgr.writeUserPhrasesAtOnce(userPhrase, areWeFiltering: action == .toFilter) {
succeeded = false
}

View File

@ -79,13 +79,12 @@ public extension SessionCtl {
func showCandidates() {
guard client() != nil else { return }
updateVerticalTypingStatus()
isVerticalCandidateWindow = (isVerticalTyping || !PrefMgr.shared.useHorizontalCandidateList)
let isServiceMenu = state.type == .ofSymbolTable && state.node.containsCandidateServices
isVerticalCandidateWindow = isVerticalTyping || !PrefMgr.shared.useHorizontalCandidateList
isVerticalCandidateWindow = isVerticalCandidateWindow || isServiceMenu
/// IMK
let candidateLayout: NSUserInterfaceLayoutOrientation =
((isVerticalTyping || !PrefMgr.shared.useHorizontalCandidateList)
? .vertical
: .horizontal)
let candidateLayout: NSUserInterfaceLayoutOrientation = (isVerticalCandidateWindow ? .vertical : .horizontal)
let isInputtingWithCandidates = state.type == .ofInputting && state.isCandidateContainer
/// NSWindow()
@ -93,6 +92,8 @@ public extension SessionCtl {
candidateUI = CtlCandidateTDK(candidateLayout)
var singleLine = isVerticalTyping || PrefMgr.shared.candidateWindowShowOnlyOneLine
singleLine = singleLine || isInputtingWithCandidates
singleLine = singleLine || isServiceMenu
(candidateUI as? CtlCandidateTDK)?.maxLinesPerPage = singleLine ? 1 : 4
candidateUI?.candidateFont = Self.candidateFont(

View File

@ -6,10 +6,10 @@
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import CocoaExtension
import IMKUtils
import InputMethodKit
import NotifierUI
import OSFrameworkImpl
import Shared
import SwiftyCapsLockToggler
@ -84,6 +84,7 @@ public extension SessionCtl {
if PrefMgr.shared.shiftEisuToggleOffTogetherWithCapsLock, !isCapsLockTurnedOn, self?.isASCIIMode ?? false {
self?.isASCIIMode.toggle()
}
self?.resetInputHandler()
guard PrefMgr.shared.showNotificationsWhenTogglingCapsLock else { return }
guard !PrefMgr.shared.bypassNonAppleCapsLockHandling else { return }
let status = NSLocalizedString("NotificationSwitchRevolver", comment: "")

View File

@ -7,8 +7,8 @@
// requirements defined in MIT License.
import AppKit
import CocoaExtension
import NotifierUI
import OSFrameworkImpl
import Shared
import SwiftExtension
@ -45,6 +45,11 @@ extension SessionCtl {
.act(#selector(toggleAssociatedPhrasesEnabled(_:)))
.state(PrefMgr.shared.associatedPhrasesEnabled)
.hotkey(PrefMgr.shared.usingHotKeyAssociates ? "O" : "", mask: [.command, .control])
NSMenu.Item("Edit Associated Phrases…")?
.act(#selector(openAssociatedPhrases(_:)))
.alternated()
.hotkey(PrefMgr.shared.usingHotKeyAssociates ? "O" : "", mask: [.command, .option, .control])
.nulled(silentMode)
NSMenu.Item("CIN Cassette Mode")?
.act(#selector(toggleCassetteMode(_:)))
.state(PrefMgr.shared.cassetteEnabled)
@ -74,47 +79,73 @@ extension SessionCtl {
NSMenu.Item("Use Phrase Replacement")?
.act(#selector(togglePhraseReplacement(_:)))
.state(PrefMgr.shared.phraseReplacementEnabled)
.nulled(!optionKeyPressed && !PrefMgr.shared.phraseReplacementEnabled)
NSMenu.Item("Edit Phrase Replacement Table…")?
.act(#selector(openPhraseReplacement(_:)))
.alternated().nulled(silentMode)
NSMenu.Item("Symbol & Emoji Input")?
.act(#selector(toggleSymbolEnabled(_:)))
.state(PrefMgr.shared.symbolInputEnabled)
.nulled(!optionKeyPressed)
NSMenu.Item("Edit User Symbol & Emoji Data…")?
.act(#selector(openUserSymbols(_:)))
.alternated().nulled(silentMode)
NSMenu.Item.separator() // ---------------------
NSMenu.Item("Open User Dictionary Folder")?.act(#selector(openUserDataFolder(_:))).nulled(silentMode)
NSMenu.Item("Edit vChewing User Phrases…")?.act(#selector(openUserPhrases(_:))).nulled(silentMode)
NSMenu.Item("Edit Excluded Phrases…")?.act(#selector(openExcludedPhrases(_:))).nulled(silentMode)
NSMenu.Item("Edit Associated Phrases…")?.act(#selector(openAssociatedPhrases(_:))).nulled(
!(!silentMode && (optionKeyPressed || PrefMgr.shared.associatedPhrasesEnabled))
)
NSMenu.Item("Edit Phrase Replacement Table…")?.act(#selector(openPhraseReplacement(_:))).nulled(silentMode || !optionKeyPressed)
NSMenu.Item("Edit User Symbol & Emoji Data…")?.act(#selector(openUserSymbols(_:))).nulled(silentMode || !optionKeyPressed)
NSMenu.Item("Open App Support Folder")?.act(#selector(openAppSupportFolderFromContainer(_:))).nulled(silentMode || !optionKeyPressed)
NSMenu.Item("Reload User Phrases")?.act(#selector(reloadUserPhrasesData(_:))).nulled(PrefMgr.shared.shouldAutoReloadUserDataFiles && !optionKeyPressed)
NSMenu.Item("Open User Dictionary Folder")?
.act(#selector(openUserDataFolder(_:)))
.nulled(silentMode)
NSMenu.Item("Open App Support Folder")?
.act(#selector(openAppSupportFolderFromContainer(_:)))
.alternated().nulled(silentMode)
NSMenu.Item("Edit vChewing User Phrases…")?
.act(#selector(openUserPhrases(_:)))
.nulled(silentMode)
NSMenu.Item("Reload User Phrases")?
.act(#selector(reloadUserPhrasesData(_:)))
NSMenu.Item("Edit Excluded Phrases…")?
.act(#selector(openExcludedPhrases(_:)))
.alternated().nulled(silentMode)
NSMenu.Item(verbatim: "Reverse Lookup (Phonabets)".localized.withEllipsis)?
.act(#selector(callReverseLookupWindow(_:))).hotkey(PrefMgr.shared.usingHotKeyRevLookup ? "/" : "", mask: [.command, .control])
NSMenu.Item("Optimize Memorized Phrases")?.act(#selector(removeUnigramsFromUOM(_:)))
NSMenu.Item("Clear Memorized Phrases")?.act(#selector(clearUOM(_:)))
.act(#selector(callReverseLookupWindow(_:)))
.hotkey(PrefMgr.shared.usingHotKeyRevLookup ? "/" : "", mask: [.command, .control])
NSMenu.Item("Optimize Memorized Phrases")?
.act(#selector(removeUnigramsFromUOM(_:)))
NSMenu.Item("Clear Memorized Phrases")?
.act(#selector(clearUOM(_:)))
.alternated()
NSMenu.Item.separator() // ---------------------
if #unavailable(macOS 13) {
NSMenu.Item("vChewing Preferences…")?.act(#selector(showPreferences(_:))).nulled(silentMode)
NSMenu.Item("vChewing Preferences…")?
.act(#selector(showPreferences(_:)))
.nulled(silentMode)
} else {
NSMenu.Item(
verbatim: "vChewing Preferences…".localized + " (SwiftUI)"
)?.act(#selector(showSettingsSwiftUI(_:))).nulled(silentMode)
NSMenu.Item(
verbatim: "vChewing Preferences…".localized + " (AppKit)"
)?.act(#selector(showSettingsAppKit(_:))).nulled(silentMode)
NSMenu.Item(verbatim: "vChewing Preferences…".localized + " (SwiftUI)")?
.act(#selector(showSettingsSwiftUI(_:)))
.nulled(silentMode)
NSMenu.Item(verbatim: "vChewing Preferences…".localized + " (AppKit)")?
.act(#selector(showSettingsAppKit(_:)))
.alternated().nulled(silentMode)
}
NSMenu.Item(verbatim: "Client Manager".localized.withEllipsis)?.act(#selector(showClientListMgr(_:))).nulled(silentMode)
NSMenu.Item("Check for Updates…")?.act(#selector(checkForUpdate(_:))).nulled(silentMode)
NSMenu.Item("Reboot vChewing…")?.act(#selector(selfTerminate(_:)))
NSMenu.Item("About vChewing…")?.act(#selector(showAbout(_:))).nulled(silentMode)
NSMenu.Item("CheatSheet")?.act(#selector(showCheatSheet(_:))).nulled(silentMode)
NSMenu.Item("Uninstall vChewing…")?.act(#selector(selfUninstall(_:))).nulled(silentMode || !optionKeyPressed)
NSMenu.Item(verbatim: "Client Manager".localized.withEllipsis)?
.act(#selector(showClientListMgr(_:)))
.nulled(silentMode)
NSMenu.Item(verbatim: "Service Menu Editor".localized.withEllipsis)?
.act(#selector(showServiceMenuEditor(_:)))
.alternated().nulled(silentMode)
NSMenu.Item("Check for Updates…")?
.act(#selector(checkForUpdate(_:)))
.nulled(silentMode)
NSMenu.Item("Reboot vChewing…")?
.act(#selector(selfTerminate(_:)))
NSMenu.Item("CheatSheet")?
.act(#selector(showCheatSheet(_:)))
.nulled(silentMode)
NSMenu.Item("About vChewing…")?
.act(#selector(showAbout(_:)))
.nulled(silentMode)
NSMenu.Item("Uninstall vChewing…")?
.act(#selector(selfUninstall(_:)))
.nulled(silentMode || !optionKeyPressed)
}
}
}
@ -156,6 +187,11 @@ public extension SessionCtl {
NSApp.popup()
}
@objc func showServiceMenuEditor(_: Any? = nil) {
CtlServiceMenuEditor.show()
NSApp.popup()
}
@objc func toggleCassetteMode(_: Any? = nil) {
resetInputHandler(forceComposerCleanup: true)
if !PrefMgr.shared.cassetteEnabled, !LMMgr.checkCassettePathValidity(PrefMgr.shared.cassettePath) {

View File

@ -92,7 +92,7 @@ extension CtlSettingsCocoa: NSToolbarDelegate {
func use(view newView: NSView, animate: Bool = true) {
guard let window = window, let existingContentView = window.contentView else { return }
guard previousView != newView else { return }
newView.layoutSubtreeIfNeeded()
newView.layoutSubtreeIfNeeded() // macOS 10.9
previousView = newView
let temporaryViewOld = NSView(frame: existingContentView.frame)
window.contentView = temporaryViewOld
@ -101,6 +101,7 @@ extension CtlSettingsCocoa: NSToolbarDelegate {
newWindowRect.origin.y = window.frame.maxY - newWindowRect.height
window.setFrame(newWindowRect, display: true, animate: animate)
window.contentView = newView
newView.layoutSubtreeIfNeeded() //
}
var toolbarIdentifiers: [NSToolbarItem.Identifier] {

View File

@ -20,6 +20,12 @@ public class SettingsPanesCocoa {
public let ctlPageCassette = SettingsPanesCocoa.Cassette()
public let ctlPageKeyboard = SettingsPanesCocoa.Keyboard()
public let ctlPageDevZone = SettingsPanesCocoa.DevZone()
public static let windowWidth: CGFloat = 614
public static var contentWidth: CGFloat { windowWidth - 65 }
public static var innerContentWidth: CGFloat { contentWidth - 37 }
public static var tabContainerWidth: CGFloat { contentWidth + 20 }
public static var contentHalfWidth: CGFloat { contentWidth / 2 - 4 }
}
public extension SettingsPanesCocoa {
@ -38,7 +44,7 @@ public extension SettingsPanesCocoa {
static func warnAboutComDlg32Inavailability() {
let title = "Please drag the desired target from Finder to this place.".localized
let message = "[Technical Reason] macOS releases earlier than 10.13 have an issue: If calling NSOpenPanel directly from an input method, both the input method and its current client app hang in a dead-loop. Furthermore, it makes other apps hang in the same way when you switch into another app. If you don't want to hard-reboot your computer, your last resort is to use SSH to connect to your current computer from another computer and kill the input method process by Terminal commands. That's why vChewing cannot offer access to NSOpenPanel for macOS 10.12 and earlier.".localized
NSApp.keyWindow.callAlert(title: title, text: message)
CtlSettingsCocoa.shared?.window.callAlert(title: title, text: message)
}
}

View File

@ -12,9 +12,11 @@ import Shared
public extension SettingsPanesCocoa {
class Behavior: NSViewController {
let windowWidth: CGFloat = 577
let contentWidth: CGFloat = 512 - 37
let tabContainerWidth: CGFloat = 512 + 20
var windowWidth: CGFloat { SettingsPanesCocoa.windowWidth }
var contentWidth: CGFloat { SettingsPanesCocoa.contentWidth }
var innerContentWidth: CGFloat { SettingsPanesCocoa.innerContentWidth }
var tabContainerWidth: CGFloat { SettingsPanesCocoa.tabContainerWidth }
var contentHalfWidth: CGFloat { SettingsPanesCocoa.contentHalfWidth }
override public func loadView() {
view = body ?? .init()
@ -27,35 +29,39 @@ public extension SettingsPanesCocoa {
NSView().makeSimpleConstraint(.height, relation: .equal, value: 4)
NSTabView.build {
NSTabView.TabPage(title: "") {
NSStackView.buildSection(width: contentWidth) {
UserDef.kSpecifyShiftBackSpaceKeyBehavior.render(fixWidth: contentWidth)
UserDef.kSpecifyShiftTabKeyBehavior.render(fixWidth: contentWidth)
UserDef.kSpecifyShiftSpaceKeyBehavior.render(fixWidth: contentWidth)
}?.boxed()
NSStackView.buildSection(width: contentWidth) {
UserDef.kUpperCaseLetterKeyBehavior.render(fixWidth: contentWidth)
UserDef.kNumPadCharInputBehavior.render(fixWidth: contentWidth)
}?.boxed()
NSStackView.buildSection(width: contentWidth) {
UserDef.kSpecifyIntonationKeyBehavior.render(fixWidth: contentWidth)
UserDef.kAcceptLeadingIntonations.render(fixWidth: contentWidth)
NSStackView.buildSection(width: innerContentWidth) {
UserDef.kSpecifyShiftBackSpaceKeyBehavior.render(fixWidth: innerContentWidth)
UserDef.kSpecifyShiftTabKeyBehavior.render(fixWidth: innerContentWidth)
UserDef.kSpecifyShiftSpaceKeyBehavior.render(fixWidth: innerContentWidth)
UserDef.kSpecifyCmdOptCtrlEnterBehavior.render(fixWidth: innerContentWidth)
}?.boxed()
NSView()
}
NSTabView.TabPage(title: "") {
NSStackView.buildSection(width: contentWidth) {
UserDef.kChooseCandidateUsingSpace.render(fixWidth: contentWidth)
UserDef.kEscToCleanInputBuffer.render(fixWidth: contentWidth)
UserDef.kAlsoConfirmAssociatedCandidatesByEnter.render(fixWidth: contentWidth)
UserDef.kUseSpaceToCommitHighlightedSCPCCandidate.render(fixWidth: contentWidth)
NSStackView.buildSection(width: innerContentWidth) {
UserDef.kUpperCaseLetterKeyBehavior.render(fixWidth: innerContentWidth)
UserDef.kNumPadCharInputBehavior.render(fixWidth: innerContentWidth)
}?.boxed()
NSStackView.buildSection(width: contentWidth) {
NSStackView.buildSection(width: innerContentWidth) {
UserDef.kSpecifyIntonationKeyBehavior.render(fixWidth: innerContentWidth)
UserDef.kAcceptLeadingIntonations.render(fixWidth: innerContentWidth)
}?.boxed()
NSView()
}
NSTabView.TabPage(title: "") {
NSStackView.buildSection(width: innerContentWidth) {
UserDef.kChooseCandidateUsingSpace.render(fixWidth: innerContentWidth)
UserDef.kEscToCleanInputBuffer.render(fixWidth: innerContentWidth)
UserDef.kAlsoConfirmAssociatedCandidatesByEnter.render(fixWidth: innerContentWidth)
UserDef.kUseSpaceToCommitHighlightedSCPCCandidate.render(fixWidth: innerContentWidth)
}?.boxed()
NSStackView.buildSection(width: innerContentWidth) {
if #available(macOS 12, *) {
UserDef.kShowNotificationsWhenTogglingCapsLock.render(fixWidth: contentWidth)
UserDef.kShowNotificationsWhenTogglingCapsLock.render(fixWidth: innerContentWidth)
}
UserDef.kAlwaysShowTooltipTextsHorizontally.render(fixWidth: contentWidth)
UserDef.kAlwaysShowTooltipTextsHorizontally.render(fixWidth: innerContentWidth)
if Date.isTodayTheDate(from: 0401) {
UserDef.kShouldNotFartInLieuOfBeep.render { renderable in
UserDef.kShouldNotFartInLieuOfBeep.render(fixWidth: innerContentWidth) { renderable in
renderable.currentControl?.target = self
renderable.currentControl?.action = #selector(self.onFartControlChange(_:))
}
@ -63,30 +69,32 @@ public extension SettingsPanesCocoa {
}?.boxed()
NSView()
}
NSTabView.TabPage(title: "") {
NSStackView.buildSection(width: contentWidth) {
UserDef.kBypassNonAppleCapsLockHandling.render(fixWidth: contentWidth)
UserDef.kShareAlphanumericalModeStatusAcrossClients.render(fixWidth: contentWidth)
NSTabView.TabPage(title: "") {
NSStackView.buildSection(width: innerContentWidth) {
UserDef.kBypassNonAppleCapsLockHandling.render(fixWidth: innerContentWidth)
UserDef.kShareAlphanumericalModeStatusAcrossClients.render(fixWidth: innerContentWidth)
if #available(macOS 10.15, *) {
NSStackView.build(.vertical) {
UserDef.kTogglingAlphanumericalModeWithLShift.render { renderable in
renderable.currentControl?.target = self
renderable.currentControl?.action = #selector(self.syncShiftKeyUpChecker(_:))
}
UserDef.kTogglingAlphanumericalModeWithRShift.render { renderable in
renderable.currentControl?.target = self
renderable.currentControl?.action = #selector(self.syncShiftKeyUpChecker(_:))
}
UserDef.kTogglingAlphanumericalModeWithLShift
.render(fixWidth: innerContentWidth) { renderable in
renderable.currentControl?.target = self
renderable.currentControl?.action = #selector(self.syncShiftKeyUpChecker(_:))
}
UserDef.kTogglingAlphanumericalModeWithRShift
.render(fixWidth: innerContentWidth) { renderable in
renderable.currentControl?.target = self
renderable.currentControl?.action = #selector(self.syncShiftKeyUpChecker(_:))
}
var strOSReq = ""
strOSReq += String(
format: "This feature requires macOS %@ and above.".localized, arguments: ["10.15"]
)
strOSReq += "\n"
strOSReq += "i18n:settings.shiftKeyASCIITogle.description".localized
strOSReq.makeNSLabel(descriptive: true, fixWidth: contentWidth)
strOSReq.makeNSLabel(descriptive: true, fixWidth: innerContentWidth)
}
}
UserDef.kShiftEisuToggleOffTogetherWithCapsLock.render(fixWidth: contentWidth)
UserDef.kShiftEisuToggleOffTogetherWithCapsLock.render(fixWidth: innerContentWidth)
}?.boxed()
NSView()
}
@ -116,7 +124,7 @@ public extension SettingsPanesCocoa {
}
}
alert.addButton(withTitle: NSLocalizedString("Leave it checked", comment: ""))
let window = NSApp.keyWindow
let window = CtlSettingsCocoa.shared?.window
if !PrefMgr.shared.shouldNotFartInLieuOfBeep {
PrefMgr.shared.shouldNotFartInLieuOfBeep = true
alert.beginSheetModal(at: window) { result in

Some files were not shown because too many files have changed in this diff Show More