Compare commits
No commits in common. "main" and "3.8.0" have entirely different histories.
|
@ -1,4 +1,4 @@
|
|||
name: debug-macOS-MainAssembly
|
||||
name: Build-with-macOS-latest
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
|
@ -17,6 +17,6 @@ jobs:
|
|||
xcode-version: '^15.1'
|
||||
- uses: actions/checkout@v1
|
||||
- name: Clean
|
||||
run: make spmClean
|
||||
run: make clean
|
||||
- name: Build
|
||||
run: make spmDebug
|
||||
run: git pull --all && git submodule sync; make update; make
|
||||
|
|
|
@ -194,7 +194,7 @@ func prepareDatabase() -> Bool {
|
|||
PRIMARY KEY (theChar)
|
||||
) WITHOUT ROWID;
|
||||
"""
|
||||
guard sqlite3_open(":memory:", &ptrSQL) == SQLITE_OK else { return false }
|
||||
guard sqlite3_open(urlSQLite, &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,20 +231,6 @@ 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] {
|
||||
|
@ -1058,19 +1044,6 @@ 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
|
||||
|
@ -1093,71 +1066,57 @@ 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()
|
||||
|
|
|
@ -10,8 +10,6 @@ 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
|
||||
|
@ -55,7 +53,6 @@ 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)
|
||||
}
|
||||
}
|
||||
|
|
19
Makefile
19
Makefile
|
@ -11,24 +11,6 @@ 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
|
||||
|
||||
|
@ -60,7 +42,6 @@ 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 +0,0 @@
|
|||
BasedOnStyle: Microsoft
|
|
@ -1,104 +0,0 @@
|
|||
# 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
|
|
@ -13,13 +13,13 @@ let package = Package(
|
|||
),
|
||||
],
|
||||
dependencies: [
|
||||
.package(path: "../vChewing_OSFrameworkImpl"),
|
||||
.package(path: "../vChewing_CocoaExtension"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "NSAttributedTextView",
|
||||
dependencies: [
|
||||
.product(name: "OSFrameworkImpl", package: "vChewing_OSFrameworkImpl"),
|
||||
.product(name: "CocoaExtension", package: "vChewing_CocoaExtension"),
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
// Modified by The vChewing Project in order to use it with AppKit.
|
||||
|
||||
import AppKit
|
||||
import OSFrameworkImpl
|
||||
import CocoaExtension
|
||||
import SwiftUI
|
||||
|
||||
@available(macOS 10.15, *)
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import AppKit
|
||||
import CocoaExtension
|
||||
import Foundation
|
||||
@testable import NSAttributedTextView
|
||||
import OSFrameworkImpl
|
||||
import Shared
|
||||
import XCTest
|
||||
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
+.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
|
|
@ -1,6 +1,7 @@
|
|||
// (c) 2019 and onwards Robert Muckle-Jones (Apache 2.0 License).
|
||||
|
||||
import Foundation
|
||||
import SwiftExtension
|
||||
|
||||
public class LineReader {
|
||||
let encoding: String.Encoding
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/configuration/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
|
@ -1,35 +0,0 @@
|
|||
// 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"]
|
||||
),
|
||||
]
|
||||
)
|
|
@ -1,284 +0,0 @@
|
|||
// (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 國通標準並未定義花括弧。
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
// (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
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
// (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, "⠙⠔⠆⠅⠳⠁⠅⠳⠁⠃⠡⠆⠇⠢⠗⠆⠅⠳⠁⠅⠳⠁⠅⠜⠂⠐⠎⠧⠁⠅⠳⠁⠅⠳⠁⠉⠪⠄⠜⠆⠎⠆⠅⠳⠁⠅⠳⠁⠖⠂⠐⠆")
|
||||
}
|
||||
}
|
|
@ -19,7 +19,6 @@ 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
|
||||
|
@ -82,8 +81,7 @@ public class CandidateCellData: Hashable {
|
|||
}
|
||||
|
||||
public func cellLength(isMatrix: Bool = true) -> Double {
|
||||
let factor: CGFloat = (Self.internalPrefs.minCellWidthForHorizontalMatrix == 0) ? 1.5 : 2
|
||||
let minLength = ceil(Self.unifiedCharDimension * factor + size * 1.25)
|
||||
let minLength = ceil(Self.unifiedCharDimension * 2 + size * 1.25)
|
||||
if displayedText.count <= 2, isMatrix { return minLength }
|
||||
return textDimension.width
|
||||
}
|
||||
|
@ -202,14 +200,14 @@ public class CandidateCellData: Hashable {
|
|||
return attrStrCandidate
|
||||
}
|
||||
|
||||
public func charDescriptions(shortened: Bool = false) -> [String] {
|
||||
public var charDescriptions: [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 shortened ? String(format: "U+%02X", $0.value) : String(format: "U+%02X %@", $0.value, theName)
|
||||
return String(format: "U+%02X %@", $0.value, theName)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -301,12 +301,10 @@ 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))
|
||||
addedCounter += 1
|
||||
if maxLinesPerPage == 1, addedCounter == 2 { break }
|
||||
if maxLinesPerPage == 1 { break }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -17,24 +17,26 @@ open class CtlCandidate: NSWindowController, CtlCandidateProtocol {
|
|||
open var reverseLookupResult: [String] = []
|
||||
|
||||
open func highlightedColor() -> NSColor {
|
||||
var result = NSColor.clear
|
||||
if #available(macOS 10.14, *) {
|
||||
result = .controlAccentColor
|
||||
} else {
|
||||
result = .alternateSelectedControlTextColor
|
||||
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
|
||||
}
|
||||
let colorBlendAmount = 0.3
|
||||
// 設定當前高亮候選字的背景顏色。
|
||||
switch locale {
|
||||
case "zh-Hans":
|
||||
result = NSColor.red
|
||||
result = NSColor.systemRed
|
||||
case "zh-Hant":
|
||||
result = NSColor.blue
|
||||
result = NSColor.systemBlue
|
||||
case "ja":
|
||||
result = NSColor.brown
|
||||
result = NSColor.systemBrown
|
||||
default: break
|
||||
}
|
||||
let blendingAgainstTarget: NSColor = NSApplication.isDarkMode ? NSColor.black : NSColor.white
|
||||
var blendingAgainstTarget: NSColor = NSApplication.isDarkMode ? NSColor.black : NSColor.white
|
||||
if #unavailable(macOS 10.14) {
|
||||
colorBlendAmount = 0.3
|
||||
blendingAgainstTarget = NSColor.white
|
||||
}
|
||||
return result.blended(withFraction: colorBlendAmount, of: blendingAgainstTarget)!
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import AppKit
|
||||
import OSFrameworkImpl
|
||||
import CocoaExtension
|
||||
import Shared
|
||||
|
||||
private extension NSUserInterfaceLayoutOrientation {
|
||||
|
@ -108,34 +108,16 @@ 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) {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import AppKit
|
||||
import OSFrameworkImpl
|
||||
import CocoaExtension
|
||||
import Shared
|
||||
|
||||
/// 田所選字窗的 AppKit 简单版本,繪製效率不受 SwiftUI 的限制。
|
||||
|
|
|
@ -2,14 +2,14 @@
|
|||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "OSFrameworkImpl",
|
||||
name: "CocoaExtension",
|
||||
platforms: [
|
||||
.macOS(.v11),
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
name: "OSFrameworkImpl",
|
||||
targets: ["OSFrameworkImpl"]
|
||||
name: "CocoaExtension",
|
||||
targets: ["CocoaExtension"]
|
||||
),
|
||||
],
|
||||
dependencies: [
|
||||
|
@ -17,7 +17,7 @@ let package = Package(
|
|||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "OSFrameworkImpl",
|
||||
name: "CocoaExtension",
|
||||
dependencies: [
|
||||
.product(name: "SwiftExtension", package: "vChewing_SwiftExtension"),
|
||||
]
|
|
@ -1,4 +1,4 @@
|
|||
# OSFrameworkImpl
|
||||
# CocoaExtension
|
||||
|
||||
威注音輸入法針對 Cocoa 的一些功能擴充,使程式維護體驗更佳。
|
||||
|
|
@ -71,18 +71,6 @@ public extension NSAttributedString {
|
|||
|
||||
public extension NSString {
|
||||
var localized: String { NSLocalizedString(description, comment: "") }
|
||||
|
||||
@objc func getCharDescriptions(_: Any? = nil) -> [String] {
|
||||
(self as String).charDescriptions
|
||||
}
|
||||
|
||||
@objc func getCodePoints(_: Any? = nil) -> [String] {
|
||||
(self as String).codePoints
|
||||
}
|
||||
|
||||
@objc func getDescriptionAsCodePoints(_: Any? = nil) -> [String] {
|
||||
(self as String).describedAsCodePoints
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - NSRange Extension
|
||||
|
@ -140,14 +128,15 @@ public extension NSApplication {
|
|||
// MARK: - System Dark Mode Status Detector.
|
||||
|
||||
static var isDarkMode: Bool {
|
||||
// "NSApp" can be nil during SPM unit tests.
|
||||
// Therefore, the method dedicated for macOS 10.15 and later is not considered stable anymore.
|
||||
// Fortunately, the method for macOS 10.14 works well on later macOS releases.
|
||||
if #available(macOS 10.14, *), let strAIS = UserDefaults.current.string(forKey: "AppleInterfaceStyle") {
|
||||
return strAIS.lowercased().contains("dark")
|
||||
} else {
|
||||
return false
|
||||
if #unavailable(macOS 10.14) { return false }
|
||||
if #available(macOS 10.15, *) {
|
||||
let appearanceDescription = NSApp.effectiveAppearance.debugDescription
|
||||
.lowercased()
|
||||
return appearanceDescription.contains("dark")
|
||||
} else if let appleInterfaceStyle = UserDefaults.current.string(forKey: "AppleInterfaceStyle") {
|
||||
return appleInterfaceStyle.lowercased().contains("dark")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MARK: - Tell whether this IME is running with Root privileges.
|
||||
|
@ -210,6 +199,19 @@ public extension NSApplication {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - String.applyingTransform
|
||||
|
||||
public extension String {
|
||||
func applyingTransformFW2HW(reverse: Bool) -> String {
|
||||
if #available(macOS 10.11, *) {
|
||||
return applyingTransform(.fullwidthToHalfwidth, reverse: reverse) ?? self
|
||||
}
|
||||
let theString = NSMutableString(string: self)
|
||||
CFStringTransform(theString, nil, kCFStringTransformFullwidthHalfwidth, reverse)
|
||||
return theString as String
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Check whether current date is the given date.
|
||||
|
||||
public extension Date {
|
||||
|
@ -325,13 +327,3 @@ public extension NSApplication {
|
|||
UserDefaults.standard.object(forKey: "AppleAccentColor") != nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Pasteboard Type Extension.
|
||||
|
||||
public extension NSPasteboard.PasteboardType {
|
||||
static let kUTTypeFileURL = Self(rawValue: "public.file-url") // import UniformTypeIdentifiers
|
||||
static let kUTTypeData = Self(rawValue: "public.data") // import UniformTypeIdentifiers
|
||||
static let kUTTypeAppBundle = Self(rawValue: "com.apple.application-bundle") // import UniformTypeIdentifiers
|
||||
static let kUTTypeUTF8PlainText = Self(rawValue: "public.utf8-plain-text")
|
||||
static let kNSFilenamesPboardType = Self(rawValue: "NSFilenamesPboardType")
|
||||
}
|
|
@ -62,59 +62,14 @@ public extension NSEdgeInsets {
|
|||
public extension NSView {
|
||||
@discardableResult func makeSimpleConstraint(
|
||||
_ attribute: NSLayoutConstraint.Attribute,
|
||||
relation givenRelation: NSLayoutConstraint.Relation,
|
||||
relation: NSLayoutConstraint.Relation,
|
||||
value: CGFloat?
|
||||
) -> NSView {
|
||||
guard let value = value else { return self }
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
guard let givenValue = value, givenValue >= 0 else { return self }
|
||||
var handled = false
|
||||
constraints.forEach { constraint in
|
||||
guard constraint.firstAttribute == attribute else { return }
|
||||
switch (constraint.relation, givenRelation) {
|
||||
case (.lessThanOrEqual, .lessThanOrEqual):
|
||||
constraint.constant = Swift.min(givenValue, constraint.constant)
|
||||
handled = true
|
||||
case (.lessThanOrEqual, .equal):
|
||||
constraint.constant = Swift.max(givenValue, constraint.constant)
|
||||
handled = true
|
||||
case (.lessThanOrEqual, .greaterThanOrEqual):
|
||||
switch givenValue {
|
||||
case constraint.constant, ..<constraint.constant: // Smaller & Equal
|
||||
handled = false
|
||||
default: // Bigger
|
||||
removeConstraint(constraint)
|
||||
handled = false
|
||||
}
|
||||
case (.equal, .lessThanOrEqual):
|
||||
constraint.constant = Swift.min(givenValue, constraint.constant)
|
||||
handled = true
|
||||
case (.equal, .equal):
|
||||
constraint.constant = Swift.min(givenValue, constraint.constant) // 往往都是外圍容器最後賦值,所以取最小值。
|
||||
handled = true
|
||||
case (.equal, .greaterThanOrEqual):
|
||||
constraint.constant = Swift.max(givenValue, constraint.constant)
|
||||
handled = true
|
||||
case (.greaterThanOrEqual, .lessThanOrEqual):
|
||||
switch givenValue {
|
||||
case ..<constraint.constant: // Smaller
|
||||
removeConstraint(constraint)
|
||||
handled = false
|
||||
default: // Bigger & Equal
|
||||
handled = false
|
||||
}
|
||||
case (.greaterThanOrEqual, .equal):
|
||||
constraint.constant = Swift.max(givenValue, constraint.constant)
|
||||
handled = true
|
||||
case (.greaterThanOrEqual, .greaterThanOrEqual):
|
||||
constraint.constant = Swift.max(givenValue, constraint.constant)
|
||||
handled = true
|
||||
default: break
|
||||
}
|
||||
}
|
||||
guard !handled else { return self }
|
||||
let widthConstraint = NSLayoutConstraint(
|
||||
item: self, attribute: attribute, relatedBy: givenRelation, toItem: nil,
|
||||
attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: givenValue
|
||||
item: self, attribute: attribute, relatedBy: relation, toItem: nil,
|
||||
attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: value
|
||||
)
|
||||
addConstraint(widthConstraint)
|
||||
return self
|
||||
|
@ -148,7 +103,6 @@ public extension NSStackView {
|
|||
|
||||
static func buildSection(
|
||||
_ orientation: NSUserInterfaceLayoutOrientation = .vertical,
|
||||
spacing: CGFloat? = nil,
|
||||
width: CGFloat? = nil,
|
||||
withDividers: Bool = true,
|
||||
@ArrayBuilder<NSView?> views: () -> [NSView?]
|
||||
|
@ -167,7 +121,7 @@ public extension NSStackView {
|
|||
itemWidth = (width - splitterDelta) / CGFloat(viewsRendered.count) - 6
|
||||
}
|
||||
func giveViews() -> [NSView?] { viewsRendered }
|
||||
let result = build(orientation, divider: withDividers, spacing: spacing, width: itemWidth, views: giveViews)?
|
||||
let result = build(orientation, divider: withDividers, width: itemWidth, views: giveViews)?
|
||||
.withInsets(.new(all: 4))
|
||||
return result
|
||||
}
|
||||
|
@ -175,7 +129,6 @@ public extension NSStackView {
|
|||
static func build(
|
||||
_ orientation: NSUserInterfaceLayoutOrientation,
|
||||
divider: Bool = false,
|
||||
spacing: CGFloat? = nil,
|
||||
width: CGFloat? = nil,
|
||||
height: CGFloat? = nil,
|
||||
insets: NSEdgeInsets? = nil,
|
||||
|
@ -187,7 +140,7 @@ public extension NSStackView {
|
|||
.makeSimpleConstraint(.height, relation: .equal, value: height)
|
||||
}
|
||||
guard !result.isEmpty else { return nil }
|
||||
return result.stack(orientation, divider: divider, spacing: spacing)?.withInsets(insets)
|
||||
return result.stack(orientation, divider: divider)?.withInsets(insets)
|
||||
}
|
||||
|
||||
func withInsets(_ newValue: NSEdgeInsets?) -> NSStackView {
|
||||
|
@ -200,7 +153,6 @@ public extension Array where Element == NSView {
|
|||
func stack(
|
||||
_ orientation: NSUserInterfaceLayoutOrientation,
|
||||
divider: Bool = false,
|
||||
spacing: CGFloat? = nil,
|
||||
insets: NSEdgeInsets? = nil
|
||||
) -> NSStackView? {
|
||||
guard !isEmpty else { return nil }
|
||||
|
@ -215,7 +167,6 @@ public extension Array where Element == NSView {
|
|||
if #unavailable(macOS 10.10) {
|
||||
outerStack.spacing = Swift.max(1, outerStack.spacing) - 1
|
||||
}
|
||||
outerStack.spacing = spacing ?? outerStack.spacing
|
||||
|
||||
outerStack.setHuggingPriority(.fittingSizeCompression, for: .horizontal)
|
||||
outerStack.setHuggingPriority(.fittingSizeCompression, for: .vertical)
|
||||
|
@ -413,12 +364,6 @@ public extension NSMenuItem {
|
|||
return self
|
||||
}
|
||||
|
||||
@discardableResult func alternated(sure sured: Bool = true) -> NSMenuItem {
|
||||
isAlternate = sured
|
||||
keyEquivalentModifierMask = .option
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult func state(_ givenState: Bool) -> NSMenuItem {
|
||||
state = givenState ? .on : .off
|
||||
return self
|
||||
|
@ -449,79 +394,3 @@ public extension NSMenuItem {
|
|||
return self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - NSFileDragRetrieverButton
|
||||
|
||||
@objcMembers public class NSFileDragRetrieverButton: NSButton {
|
||||
public var postDragHandler: ((URL) -> Void) = { url in
|
||||
NSSound.beep()
|
||||
print(url.description)
|
||||
}
|
||||
|
||||
var allowedTypes: [String] = ["txt"]
|
||||
|
||||
public convenience init(
|
||||
_ givenTitle: String? = nil,
|
||||
target: AnyObject? = nil,
|
||||
action: Selector? = nil,
|
||||
postDrag: ((URL) -> Void)? = nil
|
||||
) {
|
||||
self.init(
|
||||
verbatim: givenTitle?.localized,
|
||||
target: target,
|
||||
action: action,
|
||||
postDrag: postDrag
|
||||
)
|
||||
}
|
||||
|
||||
public init(
|
||||
verbatim givenTitle: String? = nil,
|
||||
target: AnyObject? = nil,
|
||||
action: Selector? = nil,
|
||||
postDrag: ((URL) -> Void)? = nil
|
||||
) {
|
||||
super.init(frame: .zero)
|
||||
bezelStyle = .rounded
|
||||
title = givenTitle ?? "DRAG FILE TO HERE"
|
||||
registerForDraggedTypes([.kUTTypeFileURL])
|
||||
self.target = target ?? self
|
||||
self.action = action
|
||||
postDragHandler = postDrag ?? postDragHandler
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
override public func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
|
||||
checkExtension(sender) ? .copy : NSDragOperation()
|
||||
}
|
||||
|
||||
fileprivate func checkExtension(_ drag: NSDraggingInfo) -> Bool {
|
||||
guard let pasteboard = drag.draggingPasteboard.propertyList(
|
||||
forType: NSPasteboard.PasteboardType.kNSFilenamesPboardType
|
||||
) as? [String], let path = pasteboard.first else {
|
||||
return false
|
||||
}
|
||||
|
||||
let suffix = URL(fileURLWithPath: path).pathExtension
|
||||
for ext in allowedTypes {
|
||||
if ext.lowercased() == suffix {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override public func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
|
||||
guard let pasteboard = sender.draggingPasteboard.propertyList(
|
||||
forType: NSPasteboard.PasteboardType.kNSFilenamesPboardType
|
||||
) as? [String], let path = pasteboard.first else {
|
||||
print("failure")
|
||||
return false
|
||||
}
|
||||
|
||||
postDragHandler(URL(fileURLWithPath: path))
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -20,19 +20,15 @@ public class SecureEventInputSputnik {
|
|||
}
|
||||
|
||||
public static func getIORegListResults() -> String? {
|
||||
// Don't generate results under any of the following situations:
|
||||
// - Hibernation / LoggedOut / SwitchedOut / ScreenSaver situations.
|
||||
guard NSWorkspace.activationFlags.isEmpty else { return nil }
|
||||
var resultDictionaryCF: Unmanaged<CFMutableDictionary>?
|
||||
defer { resultDictionaryCF = nil }
|
||||
/// Regarding the parameter in IORegistryGetRootEntry:
|
||||
/// Both kIOMasterPortDefault and kIOMainPortDefault are 0.
|
||||
/// The latter one is similar to what `git` had done: changing "Master" to "Main".
|
||||
let statusSucceeded = IORegistryEntryCreateCFProperties(
|
||||
IORegistryGetRootEntry(0), &resultDictionaryCF, kCFAllocatorDefault, IOOptionBits(0)
|
||||
)
|
||||
let dict: CFMutableDictionary? = resultDictionaryCF?.takeRetainedValue()
|
||||
guard statusSucceeded == KERN_SUCCESS else { return nil }
|
||||
let dict = resultDictionaryCF?.takeRetainedValue()
|
||||
guard let dict: [CFString: Any] = dict as? [CFString: Any] else { return nil }
|
||||
return (dict.description)
|
||||
}
|
||||
|
@ -79,7 +75,7 @@ public extension NSWorkspace {
|
|||
|
||||
public static let hibernating = ActivationFlags(rawValue: 1 << 0)
|
||||
public static let desktopLocked = ActivationFlags(rawValue: 1 << 1)
|
||||
public static let sessionSwitchedOut = ActivationFlags(rawValue: 1 << 2)
|
||||
public static let sesssionSwitchedOut = ActivationFlags(rawValue: 1 << 2)
|
||||
public static let screenSaverRunning = ActivationFlags(rawValue: 1 << 3)
|
||||
}
|
||||
|
||||
|
@ -127,11 +123,11 @@ extension SecureEventInputSputnik {
|
|||
.store(in: &Self.combinePool)
|
||||
NSWorkspace.shared.notificationCenter
|
||||
.publisher(for: NSWorkspace.sessionDidResignActiveNotification)
|
||||
.sink { _ in NSWorkspace.activationFlags.insert(.sessionSwitchedOut) }
|
||||
.sink { _ in NSWorkspace.activationFlags.insert(.sesssionSwitchedOut) }
|
||||
.store(in: &Self.combinePool)
|
||||
NSWorkspace.shared.notificationCenter
|
||||
.publisher(for: NSWorkspace.sessionDidBecomeActiveNotification)
|
||||
.sink { _ in NSWorkspace.activationFlags.remove(.sessionSwitchedOut) }
|
||||
.sink { _ in NSWorkspace.activationFlags.remove(.sesssionSwitchedOut) }
|
||||
.store(in: &Self.combinePool)
|
||||
} else {
|
||||
Self.combinePoolCocoa.append(
|
||||
|
@ -173,13 +169,13 @@ extension SecureEventInputSputnik {
|
|||
Self.combinePoolCocoa.append(
|
||||
NSWorkspace.shared.notificationCenter
|
||||
.addObserver(forName: NSWorkspace.sessionDidResignActiveNotification, object: nil, queue: .main) { _ in
|
||||
NSWorkspace.activationFlags.insert(.sessionSwitchedOut)
|
||||
NSWorkspace.activationFlags.insert(.sesssionSwitchedOut)
|
||||
}
|
||||
)
|
||||
Self.combinePoolCocoa.append(
|
||||
NSWorkspace.shared.notificationCenter
|
||||
.addObserver(forName: NSWorkspace.sessionDidBecomeActiveNotification, object: nil, queue: .main) { _ in
|
||||
NSWorkspace.activationFlags.remove(.sessionSwitchedOut)
|
||||
NSWorkspace.activationFlags.remove(.sesssionSwitchedOut)
|
||||
}
|
||||
)
|
||||
}
|
|
@ -100,7 +100,9 @@ public class HotenkaChineseConverter {
|
|||
dictFiles = .init()
|
||||
do {
|
||||
let rawData = try Data(contentsOf: URL(fileURLWithPath: jsonDir))
|
||||
let rawJSON = try JSONDecoder().decode([String: [String: String]].self, from: rawData)
|
||||
guard let rawJSON: [String: [String: String]] = try JSONSerialization.jsonObject(with: rawData) as? [String: [String: String]] else {
|
||||
throw NSError()
|
||||
}
|
||||
dict = rawJSON
|
||||
} catch {
|
||||
NSLog("// Exception happened when reading dict json at: \(jsonDir).")
|
||||
|
|
|
@ -40,10 +40,7 @@ extension HotenkaTests {
|
|||
let testInstance: HotenkaChineseConverter = .init(dictDir: testDataPath)
|
||||
NSLog("// Loading complete. Generating json dict file.")
|
||||
do {
|
||||
let urlOutput = URL(fileURLWithPath: testDataPath + "convdict.json")
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .sortedKeys
|
||||
try encoder.encode(testInstance.dict).write(to: urlOutput, options: .atomic)
|
||||
try JSONSerialization.data(withJSONObject: testInstance.dict, options: .sortedKeys).write(to: URL(fileURLWithPath: testDataPath + "convdict.json"))
|
||||
} catch {
|
||||
NSLog("// Error on writing strings to file: \(error)")
|
||||
}
|
||||
|
|
|
@ -15,13 +15,23 @@ public enum IMKHelper {
|
|||
/// 威注音有專門統計過,實際上會有差異的英數鍵盤佈局只有這幾種。
|
||||
/// 精簡成這種清單的話,不但節省 SwiftUI 的繪製壓力,也方便使用者做選擇。
|
||||
public static let arrWhitelistedKeyLayoutsASCII: [String] = {
|
||||
var results = LatinKeyboardMappings.allCases
|
||||
if #available(macOS 10.13, *) {
|
||||
results = results.filter {
|
||||
![.qwertyUS, .qwertzGerman, .azertyFrench].contains($0)
|
||||
}
|
||||
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")
|
||||
}
|
||||
return results.map(\.rawValue)
|
||||
return result
|
||||
}()
|
||||
|
||||
public static let arrDynamicBasicKeyLayouts: [String] = [
|
||||
|
|
|
@ -8,27 +8,25 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public enum LatinKeyboardMappings: String, CaseIterable {
|
||||
public enum LatinKeyboardMappings: String {
|
||||
case qwerty = "com.apple.keylayout.ABC"
|
||||
case qwertyBritish = "com.apple.keylayout.British"
|
||||
case qwertyUS = "com.apple.keylayout.US" // 10.9 - 10.12
|
||||
case qwertyUS = "com.apple.keylayout.US"
|
||||
case azerty = "com.apple.keylayout.ABC-AZERTY"
|
||||
case azertyFrench = "com.apple.keylayout.French"
|
||||
case qwertz = "com.apple.keylayout.ABC-QWERTZ"
|
||||
case azertyFrench = "com.apple.keylayout.French" // 10.9 - 10.12
|
||||
case qwertzGerman = "com.apple.keylayout.German" // 10.9 - 10.12
|
||||
case qwertyGerman = "com.apple.keylayout.German"
|
||||
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, .qwertyBritish: return Self.dictQwerty
|
||||
case .qwerty, .qwertyUS: return Self.dictQwerty
|
||||
case .azerty, .azertyFrench: return Self.dictAzerty
|
||||
case .qwertz, .qwertzGerman: return Self.dictQwertz
|
||||
case .qwertz, .qwertyGerman: return Self.dictQwertz
|
||||
case .colemak: return Self.dictColemak
|
||||
case .dvorak, .dvorakQwertyCMD: return Self.dictDvorak
|
||||
case .dvorak: return Self.dictDvorak
|
||||
case .dvorakLeft: return Self.dictDvorakLeft
|
||||
case .dvorakRight: return Self.dictDvorakRight
|
||||
}
|
||||
|
|
|
@ -1,212 +0,0 @@
|
|||
---
|
||||
# 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
|
||||
---
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
// 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"]
|
||||
),
|
||||
]
|
||||
)
|
|
@ -1,19 +0,0 @@
|
|||
# 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.
|
|
@ -1,25 +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 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,86 +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.
|
||||
|
||||
// 免責聲明:
|
||||
// 與奇摩輸入法有關的原始碼是由 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
|
|
@ -1,46 +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.
|
||||
|
||||
// 免責聲明:
|
||||
// 與奇摩輸入法有關的原始碼是由 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
|
|
@ -1,10 +0,0 @@
|
|||
@testable import ObjcKimoCommunicator
|
||||
import XCTest
|
||||
|
||||
final class KimoDataReaderTests: XCTestCase {
|
||||
// 先運行奇摩輸入法,再跑這個測試。
|
||||
func testExample() throws {
|
||||
let shared = ObjcKimoCommunicator()
|
||||
print(shared.establishConnection())
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
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 {} \;
|
|
@ -15,7 +15,8 @@ let package = Package(
|
|||
dependencies: [
|
||||
.package(path: "../RMJay_LineReader"),
|
||||
.package(path: "../vChewing_Megrez"),
|
||||
.package(path: "../vChewing_SwiftExtension"),
|
||||
.package(path: "../vChewing_PinyinPhonaConverter"),
|
||||
.package(path: "../vChewing_Shared"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
|
@ -23,7 +24,12 @@ let package = Package(
|
|||
dependencies: [
|
||||
.product(name: "LineReader", package: "RMJay_LineReader"),
|
||||
.product(name: "Megrez", package: "vChewing_Megrez"),
|
||||
.product(name: "SwiftExtension", package: "vChewing_SwiftExtension"),
|
||||
.product(name: "Shared", package: "vChewing_Shared"),
|
||||
.product(name: "PinyinPhonaConverter", package: "vChewing_PinyinPhonaConverter"),
|
||||
],
|
||||
resources: [
|
||||
.process("Resources/sequenceDataFromEtenDOS-chs.json"),
|
||||
.process("Resources/sequenceDataFromEtenDOS-cht.json"),
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
# LangModelAssembly
|
||||
|
||||
威注音輸入法的語言模組總成套裝,以 LMAssembly 命名空間承載下述唯二對外物件:
|
||||
威注音輸入法的語言模組總成套裝。
|
||||
|
||||
- vChewingLM:總命名空間,也承載一些在套裝內共用的工具函式。
|
||||
- LMConsolidator:自動格式整理模組。
|
||||
- LMInstantiator:語言模組副本化模組,亦集成一些自身功能擴展。
|
||||
|
||||
LMAssembly 總命名空間也承載一些在套裝內共用的工具函式。
|
||||
- LMInstantiator:語言模組副本化模組。另有其日期時間擴充模組可用(對 CIN 磁帶模式無效)。
|
||||
|
||||
以下是子模組:
|
||||
|
||||
- LMAssociates:關聯詞語模組。
|
||||
- lmCassette:專門用來處理 CIN 磁帶檔案的模組,命名為「遠野」引擎。
|
||||
- LMAssociates:關聯詞語模組。
|
||||
- LMCoreEX:可以直接讀取 TXT 格式的帶有權重資料的語彙檔案的模組。
|
||||
- LMCoreJSON:專門用來讀取原廠 JSON 檔案的模組。
|
||||
- lmPlainBopomofo:專門用來讀取使用者自訂ㄅ半候選字順序覆蓋定義檔案(plist)的模組。
|
||||
- lmReplacements:專門用來讀取使用者語彙置換模式的辭典資料的模組。
|
||||
- lmUserOverride:半衰記憶模組。
|
||||
|
|
|
@ -11,31 +11,29 @@ import Foundation
|
|||
/// 工作原理:先用 InputToken.parse 分析原始字串,給出準確的 Token。
|
||||
/// 然後再讓這個 Token 用 .translated() 自我表述出轉換結果。
|
||||
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// MARK: - 正式對外投入使用的 API。
|
||||
|
||||
public extension String {
|
||||
func parseAsInputToken(isCHS: Bool) -> [String] {
|
||||
LMAssembly.InputToken.parse(from: self).map { $0.translated(isCHS: isCHS) }.flatMap { $0 }.deduplicated
|
||||
InputToken.parse(from: self).map { $0.translated(isCHS: isCHS) }.flatMap { $0 }.deduplicated
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Parser parsing raw token value to construct token.
|
||||
|
||||
extension LMAssembly.InputToken {
|
||||
static func parse(from rawToken: String) -> [LMAssembly.InputToken] {
|
||||
var result: [LMAssembly.InputToken] = []
|
||||
public extension InputToken {
|
||||
static func parse(from rawToken: String) -> [InputToken] {
|
||||
var result: [InputToken] = []
|
||||
guard rawToken.prefix(6) == "MACRO@" else { return result }
|
||||
var mapParams: [String: Int] = [:]
|
||||
let tokenComponents = rawToken.dropFirst(6).split(separator: "_").map { param in
|
||||
|
@ -71,7 +69,7 @@ extension LMAssembly.InputToken {
|
|||
|
||||
// MARK: - Parser parsing token itself.
|
||||
|
||||
extension LMAssembly.InputToken {
|
||||
public extension InputToken {
|
||||
func translated(isCHS: Bool) -> [String] {
|
||||
let locale = Locale(identifier: isCHS ? "zh-Hans" : "zh-Hant-TW")
|
||||
let formatter = DateFormatter()
|
||||
|
|
|
@ -8,8 +8,9 @@
|
|||
|
||||
import Foundation
|
||||
import LineReader
|
||||
import Shared
|
||||
|
||||
public extension LMAssembly {
|
||||
public extension vChewingLM {
|
||||
enum LMConsolidator {
|
||||
public static let kPragmaHeader = "# 𝙵𝙾𝚁𝙼𝙰𝚃 𝚘𝚛𝚐.𝚊𝚝𝚎𝚕𝚒𝚎𝚛𝙸𝚗𝚖𝚞.𝚟𝚌𝚑𝚎𝚠𝚒𝚗𝚐.𝚞𝚜𝚎𝚛𝙻𝚊𝚗𝚐𝚞𝚊𝚐𝚎𝙼𝚘𝚍𝚎𝚕𝙳𝚊𝚝𝚊.𝚏𝚘𝚛𝚖𝚊𝚝𝚝𝚎𝚍"
|
||||
|
||||
|
@ -25,19 +26,19 @@ public extension LMAssembly {
|
|||
let lineReader = try LineReader(file: fileHandle)
|
||||
for strLine in lineReader { // 不需要 i=0,因為第一遍迴圈就出結果。
|
||||
if strLine != kPragmaHeader {
|
||||
vCLMLog("Header Mismatch, Starting In-Place Consolidation.")
|
||||
vCLog("Header Mismatch, Starting In-Place Consolidation.")
|
||||
return false
|
||||
} else {
|
||||
vCLMLog("Header Verification Succeeded: \(strLine).")
|
||||
vCLog("Header Verification Succeeded: \(strLine).")
|
||||
return true
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
vCLMLog("Header Verification Failed: File Access Error.")
|
||||
vCLog("Header Verification Failed: File Access Error.")
|
||||
return false
|
||||
}
|
||||
}
|
||||
vCLMLog("Header Verification Failed: File Missing.")
|
||||
vCLog("Header Verification Failed: File Missing.")
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -50,12 +51,12 @@ public extension LMAssembly {
|
|||
let dict = try FileManager.default.attributesOfItem(atPath: path)
|
||||
if let value = dict[FileAttributeKey.size] as? UInt64 { fileSize = value }
|
||||
} catch {
|
||||
vCLMLog("EOF Fix Failed: File Missing at \(path).")
|
||||
vCLog("EOF Fix Failed: File Missing at \(path).")
|
||||
return false
|
||||
}
|
||||
guard let fileSize = fileSize else { return false }
|
||||
guard let writeFile = FileHandle(forUpdatingAtPath: path) else {
|
||||
vCLMLog("EOF Fix Failed: File Not Writable at \(path).")
|
||||
vCLog("EOF Fix Failed: File Not Writable at \(path).")
|
||||
return false
|
||||
}
|
||||
defer { writeFile.closeFile() }
|
||||
|
@ -63,11 +64,11 @@ public extension LMAssembly {
|
|||
/// 但這個函式執行完之後往往就會 consolidate() 整理格式,所以不會有差。
|
||||
writeFile.seek(toFileOffset: fileSize - 1)
|
||||
if writeFile.readDataToEndOfFile().first != 0x0A {
|
||||
vCLMLog("EOF Missing Confirmed, Start Fixing.")
|
||||
vCLog("EOF Missing Confirmed, Start Fixing.")
|
||||
var newData = Data()
|
||||
newData.append(0x0A)
|
||||
writeFile.write(newData)
|
||||
vCLMLog("EOF Successfully Assured.")
|
||||
vCLog("EOF Successfully Assured.")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -141,29 +142,14 @@ public extension LMAssembly {
|
|||
// Write consolidated file contents.
|
||||
try strProcessed.write(to: urlPath, atomically: false, encoding: .utf8)
|
||||
} catch {
|
||||
vCLMLog("Consolidation Failed w/ File: \(path), error: \(error)")
|
||||
vCLog("Consolidation Failed w/ File: \(path), error: \(error)")
|
||||
return false
|
||||
}
|
||||
vCLMLog("Either Consolidation Successful Or No-Need-To-Consolidate.")
|
||||
vCLog("Either Consolidation Successful Or No-Need-To-Consolidate.")
|
||||
return true
|
||||
}
|
||||
vCLMLog("Consolidation Failed: File Missing at \(path).")
|
||||
vCLog("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 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,8 +8,10 @@
|
|||
|
||||
import Foundation
|
||||
import Megrez
|
||||
import Shared
|
||||
import SQLite3
|
||||
|
||||
public extension LMAssembly {
|
||||
public extension vChewingLM {
|
||||
/// 語言模組副本化模組(LMInstantiator,下稱「LMI」)自身為符合天權星組字引擎內
|
||||
/// 的 LangModelProtocol 協定的模組、統籌且整理來自其它子模組的資料(包括使
|
||||
/// 用者語彙、繪文字模組、語彙濾除表、原廠語言模組等)。
|
||||
|
@ -37,40 +39,45 @@ public extension LMAssembly {
|
|||
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 internal(set) static var isSQLDBConnected: Bool = false
|
||||
public private(set) static var isSQLDBConnected: Bool = false
|
||||
|
||||
// 簡體中文模型?
|
||||
public let isCHS: Bool
|
||||
|
||||
// 在函式內部用以記錄狀態的開關。
|
||||
public private(set) var config = Config()
|
||||
public var config = Config()
|
||||
|
||||
// 這句需要留著,不然無法被 package 外界存取。
|
||||
public init(
|
||||
isCHS: Bool = false,
|
||||
uomDataURL: URL? = nil
|
||||
) {
|
||||
public init(isCHS: Bool = false) {
|
||||
self.isCHS = isCHS
|
||||
lmUserOverride = .init(dataURL: uomDataURL)
|
||||
}
|
||||
|
||||
@discardableResult public func setOptions(handler: (inout Config) -> Void) -> LMInstantiator {
|
||||
public func setOptions(handler: (inout Config) -> Void) {
|
||||
handler(&config)
|
||||
return self
|
||||
}
|
||||
|
||||
public static func setCassetCandidateKeyValidator(_ validator: @escaping (String) -> Bool) {
|
||||
Self.lmCassette.candidateKeysValidator = validator
|
||||
@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
|
||||
}
|
||||
|
||||
/// 介紹一下幾個通用的語言模組型別:
|
||||
|
@ -85,7 +92,6 @@ public extension LMAssembly {
|
|||
|
||||
// 磁帶資料模組。「currentCassette」對外唯讀,僅用來讀取磁帶本身的中繼資料(Metadata)。
|
||||
static var lmCassette = LMCassette()
|
||||
static var lmPlainBopomofo = LMPlainBopomofo()
|
||||
|
||||
// 聲明使用者語言模組。
|
||||
// 使用者語言模組使用多執行緒的話,可能會導致一些問題。有時間再仔細排查看看。
|
||||
|
@ -100,46 +106,30 @@ public extension LMAssembly {
|
|||
)
|
||||
var lmReplacements = LMReplacements()
|
||||
var lmAssociates = LMAssociates()
|
||||
|
||||
// 半衰记忆模组
|
||||
var lmUserOverride: LMUserOverride
|
||||
var lmPlainBopomofo = LMPlainBopomofo()
|
||||
|
||||
// MARK: - 工具函式
|
||||
|
||||
public func resetFactoryJSONModels() {}
|
||||
|
||||
public func loadUserPhrasesData(path: String, filterPath: String?) {
|
||||
func loadMain() {
|
||||
DispatchQueue.main.async {
|
||||
if FileManager.default.isReadableFile(atPath: path) {
|
||||
lmUserPhrases.clear()
|
||||
lmUserPhrases.open(path)
|
||||
vCLMLog("lmUserPhrases: \(lmUserPhrases.count) entries of data loaded from: \(path)")
|
||||
self.lmUserPhrases.clear()
|
||||
self.lmUserPhrases.open(path)
|
||||
vCLog("lmUserPhrases: \(self.lmUserPhrases.count) entries of data loaded from: \(path)")
|
||||
} else {
|
||||
vCLMLog("lmUserPhrases: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
if !Self.asyncLoadingUserData {
|
||||
loadMain()
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
loadMain()
|
||||
vCLog("lmUserPhrases: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
guard let filterPath = filterPath else { return }
|
||||
func loadFilter() {
|
||||
DispatchQueue.main.async {
|
||||
if FileManager.default.isReadableFile(atPath: filterPath) {
|
||||
lmFiltered.clear()
|
||||
lmFiltered.open(filterPath)
|
||||
vCLMLog("lmFiltered: \(lmFiltered.count) entries of data loaded from: \(path)")
|
||||
self.lmFiltered.clear()
|
||||
self.lmFiltered.open(filterPath)
|
||||
vCLog("lmFiltered: \(self.lmFiltered.count) entries of data loaded from: \(path)")
|
||||
} else {
|
||||
vCLMLog("lmFiltered: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
if !Self.asyncLoadingUserData {
|
||||
loadFilter()
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
loadFilter()
|
||||
vCLog("lmFiltered: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -149,85 +139,74 @@ public extension LMAssembly {
|
|||
if FileManager.default.isReadableFile(atPath: path) {
|
||||
lmFiltered.clear()
|
||||
lmFiltered.open(path)
|
||||
vCLMLog("lmFiltered: \(lmFiltered.count) entries of data loaded from: \(path)")
|
||||
vCLog("lmFiltered: \(lmFiltered.count) entries of data loaded from: \(path)")
|
||||
} else {
|
||||
vCLMLog("lmFiltered: File access failure: \(path)")
|
||||
vCLog("lmFiltered: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
|
||||
public func loadUserSymbolData(path: String) {
|
||||
func load() {
|
||||
DispatchQueue.main.async {
|
||||
if FileManager.default.isReadableFile(atPath: path) {
|
||||
lmUserSymbols.clear()
|
||||
lmUserSymbols.open(path)
|
||||
vCLMLog("lmUserSymbol: \(lmUserSymbols.count) entries of data loaded from: \(path)")
|
||||
self.lmUserSymbols.clear()
|
||||
self.lmUserSymbols.open(path)
|
||||
vCLog("lmUserSymbol: \(self.lmUserSymbols.count) entries of data loaded from: \(path)")
|
||||
} else {
|
||||
vCLMLog("lmUserSymbol: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
if !Self.asyncLoadingUserData {
|
||||
load()
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
load()
|
||||
vCLog("lmUserSymbol: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func loadUserAssociatesData(path: String) {
|
||||
func load() {
|
||||
DispatchQueue.main.async {
|
||||
if FileManager.default.isReadableFile(atPath: path) {
|
||||
lmAssociates.clear()
|
||||
lmAssociates.open(path)
|
||||
vCLMLog("lmAssociates: \(lmAssociates.count) entries of data loaded from: \(path)")
|
||||
self.lmAssociates.clear()
|
||||
self.lmAssociates.open(path)
|
||||
vCLog("lmAssociates: \(self.lmAssociates.count) entries of data loaded from: \(path)")
|
||||
} else {
|
||||
vCLMLog("lmAssociates: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
if !Self.asyncLoadingUserData {
|
||||
load()
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
load()
|
||||
vCLog("lmAssociates: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func loadReplacementsData(path: String) {
|
||||
func load() {
|
||||
DispatchQueue.main.async {
|
||||
if FileManager.default.isReadableFile(atPath: path) {
|
||||
lmReplacements.clear()
|
||||
lmReplacements.open(path)
|
||||
vCLMLog("lmReplacements: \(lmReplacements.count) entries of data loaded from: \(path)")
|
||||
self.lmReplacements.clear()
|
||||
self.lmReplacements.open(path)
|
||||
vCLog("lmReplacements: \(self.lmReplacements.count) entries of data loaded from: \(path)")
|
||||
} else {
|
||||
vCLMLog("lmReplacements: File access failure: \(path)")
|
||||
vCLog("lmReplacements: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
if !Self.asyncLoadingUserData {
|
||||
load()
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
load()
|
||||
}
|
||||
|
||||
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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var isCassetteDataLoaded: Bool { Self.lmCassette.isLoaded }
|
||||
public static func loadCassetteData(path: String) {
|
||||
func load() {
|
||||
DispatchQueue.main.async {
|
||||
if FileManager.default.isReadableFile(atPath: path) {
|
||||
Self.lmCassette.clear()
|
||||
Self.lmCassette.open(path)
|
||||
vCLMLog("lmCassette: \(Self.lmCassette.count) entries of data loaded from: \(path)")
|
||||
vCLog("lmCassette: \(Self.lmCassette.count) entries of data loaded from: \(path)")
|
||||
} else {
|
||||
vCLMLog("lmCassette: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
if !Self.asyncLoadingUserData {
|
||||
load()
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
load()
|
||||
vCLog("lmCassette: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -353,27 +332,20 @@ public extension LMAssembly {
|
|||
|
||||
// 如果有檢測到使用者自訂逐字選字語料庫內的相關資料的話,在這裡先插入。
|
||||
if config.isSCPCEnabled {
|
||||
rawAllUnigrams += Self.lmPlainBopomofo.valuesFor(key: keyChain, isCHS: isCHS).map {
|
||||
Megrez.Unigram(value: $0, score: 0)
|
||||
}
|
||||
rawAllUnigrams += lmPlainBopomofo.valuesFor(key: keyChain).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)
|
||||
// 原廠核心辭典內容。
|
||||
var coreUnigramsResult: [Megrez.Unigram] = factoryCoreUnigramsFor(key: keyChain)
|
||||
// 如果是繁體中文、且有開啟 CNS11643 全字庫讀音過濾開關的話,對原廠核心辭典內容追加過濾處理:
|
||||
if config.filterNonCNSReadings, !isCHS {
|
||||
coreUnigramsResult.removeAll { thisUnigram in
|
||||
!checkCNSConformation(for: thisUnigram, keyArray: keyArray)
|
||||
}
|
||||
}
|
||||
// 正式追加原廠核心辭典檢索結果。
|
||||
rawAllUnigrams += coreUnigramsResult
|
||||
|
||||
rawAllUnigrams += factoryCoreUnigramsFor(key: keyChain)
|
||||
if config.isCNSEnabled {
|
||||
rawAllUnigrams += factoryUnigramsFor(key: keyChain, column: .theDataCNS)
|
||||
}
|
||||
|
@ -386,21 +358,6 @@ public extension LMAssembly {
|
|||
}
|
||||
}
|
||||
|
||||
// 用 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)
|
||||
|
|
|
@ -8,9 +8,9 @@
|
|||
|
||||
import Foundation
|
||||
import Megrez
|
||||
import SwiftExtension
|
||||
import Shared
|
||||
|
||||
public extension LMAssembly.LMInstantiator {
|
||||
public extension vChewingLM.LMInstantiator {
|
||||
/// 磁帶模式專用:當前磁帶所規定的花牌鍵。
|
||||
var cassetteWildcardKey: String { Self.lmCassette.wildcardKey }
|
||||
/// 磁帶模式專用:當前磁帶規定的最大碼長。
|
||||
|
|
|
@ -11,7 +11,7 @@ import Megrez
|
|||
|
||||
// MARK: - 日期時間便捷輸入功能
|
||||
|
||||
extension LMAssembly.LMInstantiator {
|
||||
extension vChewingLM.LMInstantiator {
|
||||
func queryDateTimeUnigrams(with key: String = "") -> [Megrez.Unigram] {
|
||||
guard let tokenTrigger = TokenTrigger(rawValue: key) else { return [] }
|
||||
var results = [Megrez.Unigram]()
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import Foundation
|
||||
import Megrez
|
||||
|
||||
public extension LMAssembly.LMInstantiator {
|
||||
public extension vChewingLM.LMInstantiator {
|
||||
func supplyNumPadUnigrams(key: String) -> [Megrez.Unigram] {
|
||||
guard let status = config.numPadFWHWStatus else { return [] }
|
||||
let initials = "_NumPad_"
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import Foundation
|
||||
import Megrez
|
||||
import Shared
|
||||
import SQLite3
|
||||
|
||||
/* ==============
|
||||
|
@ -30,49 +31,30 @@ import SQLite3
|
|||
) WITHOUT ROWID;
|
||||
*/
|
||||
|
||||
extension LMAssembly.LMInstantiator {
|
||||
enum CoreColumn: Int32 {
|
||||
case theDataCHS = 1 // 簡體中文
|
||||
case theDataCHT = 2 // 繁體中文
|
||||
case theDataCNS = 3 // 全字庫
|
||||
case theDataMISC = 4 // 待辦
|
||||
case theDataSYMB = 5 // 符號圖
|
||||
case theDataCHEW = 6 // 注音文
|
||||
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 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
|
||||
}
|
||||
|
||||
extension vChewingLM.LMInstantiator {
|
||||
fileprivate static func querySQL(strStmt sqlQuery: String, coreColumn column: CoreColumn, handler: (String) -> Void) {
|
||||
guard Self.ptrSQL != nil else { return }
|
||||
performStatementSansResult { ptrStatement in
|
||||
|
@ -141,10 +123,9 @@ extension LMAssembly.LMInstantiator {
|
|||
}
|
||||
|
||||
/// 根據給定的讀音索引鍵,來獲取原廠標準資料庫辭典內的對應資料陣列的 UTF8 資料、就地分析、生成單元圖陣列。
|
||||
/// - Remark: 該函式會無損地返回原廠辭典的結果,不受使用者控頻與資料過濾條件的影響,不包含全字庫的資料。
|
||||
/// - parameters:
|
||||
/// - key: 讀音索引鍵。
|
||||
public func factoryCoreUnigramsFor(key: String) -> [Megrez.Unigram] {
|
||||
func factoryCoreUnigramsFor(key: String) -> [Megrez.Unigram] {
|
||||
// 此處需要把 ASCII 單引號換成連續兩個單引號,否則會有 SQLite 語句查詢故障。
|
||||
factoryUnigramsFor(key: key, column: isCHS ? .theDataCHS : .theDataCHT)
|
||||
}
|
||||
|
@ -153,9 +134,7 @@ extension LMAssembly.LMInstantiator {
|
|||
/// - parameters:
|
||||
/// - key: 讀音索引鍵。
|
||||
/// - column: 資料欄位。
|
||||
func factoryUnigramsFor(
|
||||
key: String, column: LMAssembly.LMInstantiator.CoreColumn
|
||||
) -> [Megrez.Unigram] {
|
||||
func factoryUnigramsFor(key: String, column: CoreColumn) -> [Megrez.Unigram] {
|
||||
if key == "_punctuation_list" { return [] }
|
||||
var grams: [Megrez.Unigram] = []
|
||||
var gramsHW: [Megrez.Unigram] = []
|
||||
|
@ -163,10 +142,8 @@ extension LMAssembly.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
|
||||
var i: Double = 0
|
||||
var previousScore: Double?
|
||||
currentResult.split(separator: "\t").forEach { strNetaSet in
|
||||
// 這裡假定原廠資料已經經過對權重的 stable sort 排序。
|
||||
let arrRangeRecords = currentResult.split(separator: "\t")
|
||||
for strNetaSet in arrRangeRecords {
|
||||
let neta = Array(strNetaSet.trimmingCharacters(in: .newlines).split(separator: " ").reversed())
|
||||
let theValue: String = .init(neta[0])
|
||||
var theScore = column.defaultScore
|
||||
|
@ -176,15 +153,8 @@ extension LMAssembly.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") { return }
|
||||
if !key.contains("_punctuation") { continue }
|
||||
let halfValue = theValue.applyingTransformFW2HW(reverse: false)
|
||||
if halfValue != theValue {
|
||||
gramsHW.append(Megrez.Unigram(value: halfValue, score: theScore))
|
||||
|
@ -195,24 +165,6 @@ extension LMAssembly.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:
|
||||
|
@ -225,22 +177,9 @@ extension LMAssembly.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 LMAssembly.LMInstantiator {
|
||||
private extension vChewingLM.LMInstantiator {
|
||||
/// 內部函式,用以將注音讀音索引鍵進行加密。
|
||||
///
|
||||
/// 使用這種加密字串作為索引鍵,可以增加對 json 資料庫的存取速度。
|
||||
|
@ -288,7 +227,7 @@ private extension LMAssembly.LMInstantiator {
|
|||
]
|
||||
}
|
||||
|
||||
public extension LMAssembly.LMInstantiator {
|
||||
public extension vChewingLM.LMInstantiator {
|
||||
@discardableResult static func connectToTestSQLDB() -> Bool {
|
||||
Self.connectSQLDB(dbPath: #":memory:"#) && sqlTestCoreLMData.runAsSQLExec(dbPointer: &ptrSQL)
|
||||
}
|
||||
|
|
|
@ -1,60 +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 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)
|
||||
}
|
||||
}
|
|
@ -1,33 +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
|
||||
|
||||
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 : [:]
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,123 +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
|
||||
|
||||
// 該檔案使得 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":"ㄓㄨㄤ"}
|
||||
}
|
||||
"""#
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -7,11 +7,13 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import Megrez
|
||||
import PinyinPhonaConverter
|
||||
import Shared
|
||||
|
||||
extension LMAssembly {
|
||||
struct LMAssociates {
|
||||
public extension vChewingLM {
|
||||
@frozen struct LMAssociates {
|
||||
public private(set) var filePath: String?
|
||||
var rangeMap: [String: [(Range<String.Index>, Int)]] = [:] // Range 只可能是一整行,所以必須得有 index。
|
||||
var rangeMap: [String: [(Range<String.Index>, Int)]] = [:]
|
||||
var strData: String = ""
|
||||
|
||||
public var count: Int { rangeMap.count }
|
||||
|
@ -46,8 +48,8 @@ extension LMAssembly {
|
|||
replaceData(textData: rawStrData)
|
||||
} catch {
|
||||
filePath = oldPath
|
||||
vCLMLog("\(error)")
|
||||
vCLMLog("↑ Exception happened when reading data at: \(path).")
|
||||
vCLog("\(error)")
|
||||
vCLog("↑ Exception happened when reading data at: \(path).")
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -91,21 +93,28 @@ extension LMAssembly {
|
|||
do {
|
||||
try strData.write(toFile: filePath, atomically: true, encoding: .utf8)
|
||||
} catch {
|
||||
vCLMLog("Failed to save current database to: \(filePath)")
|
||||
vCLog("Failed to save current database to: \(filePath)")
|
||||
}
|
||||
}
|
||||
|
||||
public func valuesFor(pair: Megrez.KeyValuePaired) -> [String] {
|
||||
var pairs: [String] = []
|
||||
let availableResults = [rangeMap[pair.toNGramKey], rangeMap[pair.value]].compactMap { $0 }
|
||||
availableResults.forEach { arrRangeRecords in
|
||||
arrRangeRecords.forEach { netaRange, index in
|
||||
if let arrRangeRecords: [(Range<String.Index>, Int)] = rangeMap[pair.toNGramKey] {
|
||||
for (netaRange, index) in arrRangeRecords {
|
||||
let neta = strData[netaRange].split(separator: " ")
|
||||
let theValue: String = .init(neta[index])
|
||||
pairs.append(theValue)
|
||||
}
|
||||
}
|
||||
return pairs.deduplicated
|
||||
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 }
|
||||
}
|
||||
|
||||
public func hasValuesFor(pair: Megrez.KeyValuePaired) -> Bool {
|
||||
|
@ -114,17 +123,3 @@ extension LMAssembly {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,10 +10,11 @@
|
|||
import Foundation
|
||||
import LineReader
|
||||
import Megrez
|
||||
import Shared
|
||||
|
||||
extension LMAssembly {
|
||||
public extension vChewingLM {
|
||||
/// 磁帶模組,用來方便使用者自行擴充字根輸入法。
|
||||
struct LMCassette {
|
||||
@frozen struct LMCassette {
|
||||
public private(set) var filePath: String?
|
||||
public private(set) var nameShort: String = ""
|
||||
public private(set) var nameENG: String = ""
|
||||
|
@ -39,13 +40,12 @@ extension LMAssembly {
|
|||
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
|
||||
}
|
||||
}
|
||||
|
||||
extension LMAssembly.LMCassette {
|
||||
public extension vChewingLM.LMCassette {
|
||||
/// 計算頻率時要用到的東西 - fscale
|
||||
private static let fscale = 2.7
|
||||
/// 萬用花牌字符,哪怕花牌鍵仍不可用。
|
||||
|
@ -86,7 +86,7 @@ extension LMAssembly.LMCassette {
|
|||
if FileManager.default.fileExists(atPath: path) {
|
||||
do {
|
||||
guard let fileHandle = FileHandle(forReadingAtPath: path) else {
|
||||
throw LMAssembly.FileErrors.fileHandleError("")
|
||||
throw vChewingLM.FileErrors.fileHandleError("")
|
||||
}
|
||||
let lineReader = try LineReader(file: fileHandle)
|
||||
var theMaxKeyLength = 1
|
||||
|
@ -195,7 +195,7 @@ extension LMAssembly.LMCassette {
|
|||
// Post process.
|
||||
// 備註:因為 Package 層級嵌套的現狀,此處不太方便檢查是否需要篩掉 J / K 鍵。
|
||||
// 因此只能在其他地方做篩檢。
|
||||
if !candidateKeysValidator(selectionKeys) { selectionKeys = "1234567890" }
|
||||
if CandidateKey.validate(keys: selectionKeys) != nil { selectionKeys = "1234567890" }
|
||||
if !keysUsedInCharDef.intersection(selectionKeys.map(\.description)).isEmpty {
|
||||
areCandidateKeysShiftHeld = true
|
||||
}
|
||||
|
@ -204,10 +204,10 @@ extension LMAssembly.LMCassette {
|
|||
filePath = path
|
||||
return true
|
||||
} catch {
|
||||
vCLMLog("CIN Loading Failed: File Access Error.")
|
||||
vCLog("CIN Loading Failed: File Access Error.")
|
||||
}
|
||||
} else {
|
||||
vCLMLog("CIN Loading Failed: File Missing.")
|
||||
vCLog("CIN Loading Failed: File Missing.")
|
||||
}
|
||||
filePath = oldPath
|
||||
return false
|
||||
|
|
|
@ -7,13 +7,15 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import Megrez
|
||||
import PinyinPhonaConverter
|
||||
import Shared
|
||||
|
||||
extension LMAssembly {
|
||||
public extension vChewingLM {
|
||||
/// 與之前的 LMCore 不同,LMCoreEX 不在辭典內記錄實體,而是記錄 range 範圍。
|
||||
/// 需要資料的時候,直接拿 range 去 strData 取資料。
|
||||
/// 資料記錄原理與上游 C++ 的 ParselessLM 差不多,但用的是 Swift 原生手段。
|
||||
/// 主要時間消耗仍在 For 迴圈,但這個算法可以顯著減少記憶體佔用。
|
||||
struct LMCoreEX {
|
||||
@frozen struct LMCoreEX {
|
||||
public private(set) var filePath: String?
|
||||
/// 資料庫辭典。索引內容為注音字串,資料內容則為字串首尾範圍、方便自 strData 取資料。
|
||||
var rangeMap: [String: [Range<String.Index>]] = [:]
|
||||
|
@ -79,8 +81,8 @@ extension LMAssembly {
|
|||
replaceData(textData: rawStrData)
|
||||
} catch {
|
||||
filePath = oldPath
|
||||
vCLMLog("\(error)")
|
||||
vCLMLog("↑ Exception happened when reading data at: \(path).")
|
||||
vCLog("\(error)")
|
||||
vCLog("↑ Exception happened when reading data at: \(path).")
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -131,7 +133,7 @@ extension LMAssembly {
|
|||
}
|
||||
try dataToWrite.write(toFile: filePath, atomically: true, encoding: .utf8)
|
||||
} catch {
|
||||
vCLMLog("Failed to save current database to: \(filePath)")
|
||||
vCLog("Failed to save current database to: \(filePath)")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -148,7 +150,7 @@ extension LMAssembly {
|
|||
strDump += addline
|
||||
}
|
||||
}
|
||||
vCLMLog(strDump)
|
||||
vCLog(strDump)
|
||||
}
|
||||
|
||||
/// 根據給定的讀音索引鍵,來獲取資料庫辭典內的對應資料陣列的字串首尾範圍資料、據此自 strData 取得字串形式的資料、生成單元圖陣列。
|
||||
|
@ -184,15 +186,3 @@ extension LMAssembly {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,36 +7,67 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import Foundation
|
||||
import Shared
|
||||
|
||||
extension LMAssembly {
|
||||
struct LMPlainBopomofo {
|
||||
@usableFromInline typealias DataMap = [String: [String: String]]
|
||||
let dataMap: DataMap
|
||||
public extension vChewingLM {
|
||||
@frozen struct LMPlainBopomofo {
|
||||
public private(set) var filePath: String?
|
||||
var dataMap: [String: String] = [:]
|
||||
|
||||
public var count: Int { dataMap.count }
|
||||
|
||||
public init() {
|
||||
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 = [:]
|
||||
}
|
||||
dataMap = [:]
|
||||
}
|
||||
|
||||
public var isLoaded: Bool { !dataMap.isEmpty }
|
||||
|
||||
public func valuesFor(key: String, isCHS: Bool) -> [String] {
|
||||
@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] {
|
||||
var pairs: [String] = []
|
||||
let subKey = isCHS ? "S" : "T"
|
||||
if let arrRangeRecords: String = dataMap[key]?[subKey] {
|
||||
if let arrRangeRecords: String = dataMap[key]?.trimmingCharacters(in: .newlines) {
|
||||
pairs.append(contentsOf: arrRangeRecords.map(\.description))
|
||||
}
|
||||
// 這裡不做去重複處理,因為倚天中文系統注音排序適應者們已經形成了肌肉記憶。
|
||||
return pairs
|
||||
return pairs.deduplicated
|
||||
}
|
||||
|
||||
public func hasValuesFor(key: String) -> Bool { dataMap.keys.contains(key) }
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -6,8 +6,10 @@
|
|||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
extension LMAssembly {
|
||||
struct LMReplacements {
|
||||
import Shared
|
||||
|
||||
public extension vChewingLM {
|
||||
@frozen struct LMReplacements {
|
||||
public private(set) var filePath: String?
|
||||
var rangeMap: [String: Range<String.Index>] = [:]
|
||||
var strData: String = ""
|
||||
|
@ -33,8 +35,8 @@ extension LMAssembly {
|
|||
replaceData(textData: rawStrData)
|
||||
} catch {
|
||||
filePath = oldPath
|
||||
vCLMLog("\(error)")
|
||||
vCLMLog("↑ Exception happened when reading data at: \(path).")
|
||||
vCLog("\(error)")
|
||||
vCLog("↑ Exception happened when reading data at: \(path).")
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -70,7 +72,7 @@ extension LMAssembly {
|
|||
do {
|
||||
try strData.write(toFile: filePath, atomically: true, encoding: .utf8)
|
||||
} catch {
|
||||
vCLMLog("Failed to save current database to: \(filePath)")
|
||||
vCLog("Failed to save current database to: \(filePath)")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,7 +81,7 @@ extension LMAssembly {
|
|||
for entry in rangeMap {
|
||||
strDump += strData[entry.value] + "\n"
|
||||
}
|
||||
vCLMLog(strDump)
|
||||
vCLog(strDump)
|
||||
}
|
||||
|
||||
public func valuesFor(key: String) -> String {
|
||||
|
@ -98,13 +100,3 @@ extension LMAssembly {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension LMAssembly.LMReplacements {
|
||||
var dictRepresented: [String: String] {
|
||||
var result = [String: String]()
|
||||
rangeMap.forEach { key, valueRange in
|
||||
result[key] = strData[valueRange].description
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
// (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": "˙",
|
||||
]
|
||||
}
|
||||
}
|
|
@ -9,41 +9,74 @@
|
|||
|
||||
import Foundation
|
||||
import Megrez
|
||||
import Shared
|
||||
|
||||
// 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 {
|
||||
public extension vChewingLM {
|
||||
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? = nil) {
|
||||
public init(capacity: Int = 500, decayConstant: Double = LMUserOverride.kObservedOverrideHalfLife, dataURL: URL) {
|
||||
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 LMAssembly.LMUserOverride {
|
||||
extension vChewingLM.LMUserOverride {
|
||||
enum OverrideUnit: CodingKey { case count, timestamp, forceHighScoreOverride }
|
||||
enum ObservationUnit: CodingKey { case count, overrides }
|
||||
enum KeyObservationPairUnit: CodingKey { case key, observation }
|
||||
|
@ -120,52 +153,10 @@ extension LMAssembly.LMUserOverride {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - Internal Methods in LMAssembly.
|
||||
// MARK: - Hash and Dehash the entire UOM data, etc.
|
||||
|
||||
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) {
|
||||
public extension vChewingLM.LMUserOverride {
|
||||
func bleachSpecifiedSuggestions(targets: [String], saveCallback: @escaping () -> Void) {
|
||||
if targets.isEmpty { return }
|
||||
for neta in mutLRUMap {
|
||||
for target in targets {
|
||||
|
@ -175,86 +166,82 @@ extension LMAssembly.LMUserOverride {
|
|||
}
|
||||
}
|
||||
resetMRUList()
|
||||
saveCallback?() ?? saveData()
|
||||
saveCallback()
|
||||
}
|
||||
|
||||
/// 自 LRU 辭典內移除所有的單元圖。
|
||||
func bleachUnigrams(saveCallback: (() -> Void)? = nil) {
|
||||
func bleachUnigrams(saveCallback: @escaping () -> Void) {
|
||||
for key in mutLRUMap.keys {
|
||||
if !key.contains("(),()") { continue }
|
||||
mutLRUMap.removeValue(forKey: key)
|
||||
}
|
||||
resetMRUList()
|
||||
saveCallback?() ?? saveData()
|
||||
saveCallback()
|
||||
}
|
||||
|
||||
func resetMRUList() {
|
||||
internal func resetMRUList() {
|
||||
mutLRUList.removeAll()
|
||||
for neta in mutLRUMap.reversed() {
|
||||
mutLRUList.append(neta.value)
|
||||
}
|
||||
}
|
||||
|
||||
func clearData(withURL fileURL: URL? = nil) {
|
||||
func clearData(withURL fileURL: URL) {
|
||||
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 {
|
||||
vCLMLog("UOM Error: Unable to clear the data in the UOM file. Details: \(error)")
|
||||
vCLog("UOM Error: Unable to clear data. 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 {
|
||||
vCLMLog("UOM Error: Unable to save data, abort saving. Details: \(error)")
|
||||
vCLog("UOM Error: Unable to save data, abort saving. Details: \(error)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
func loadData(fromURL fileURL: URL) {
|
||||
// 此處不要使用 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 {
|
||||
vCLMLog("UOM Error: Read file content type invalid, abort loading.")
|
||||
vCLog("UOM Error: Read file content type invalid, abort loading.")
|
||||
return
|
||||
}
|
||||
mutLRUMap = jsonResult
|
||||
resetMRUList()
|
||||
} catch {
|
||||
vCLMLog("UOM Error: Unable to read file or parse the data, abort loading. Details: \(error)")
|
||||
vCLog("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: - Other Non-Public Internal Methods
|
||||
// MARK: - Private Methods
|
||||
|
||||
extension LMAssembly.LMUserOverride {
|
||||
extension vChewingLM.LMUserOverride {
|
||||
func doObservation(
|
||||
key: String, candidate: String, timestamp: Double, forceHighScoreOverride: Bool,
|
||||
saveCallback: (() -> Void)?
|
||||
saveCallback: @escaping () -> Void
|
||||
) {
|
||||
guard mutLRUMap[key] != nil else {
|
||||
var observation: Observation = .init()
|
||||
|
@ -270,8 +257,8 @@ extension LMAssembly.LMUserOverride {
|
|||
mutLRUMap.removeValue(forKey: mutLRUList[mutLRUList.endIndex - 1].key)
|
||||
mutLRUList.removeLast()
|
||||
}
|
||||
vCLMLog("UOM: Observation finished with new observation: \(key)")
|
||||
saveCallback?() ?? saveData()
|
||||
vCLog("UOM: Observation finished with new observation: \(key)")
|
||||
saveCallback()
|
||||
return
|
||||
}
|
||||
// 這裡還是不要做 decayCallback 判定「是否不急著更新觀察」了,不然會在嘗試覆寫掉錯誤的記憶時失敗。
|
||||
|
@ -281,12 +268,12 @@ extension LMAssembly.LMUserOverride {
|
|||
)
|
||||
mutLRUList.insert(theNeta, at: 0)
|
||||
mutLRUMap[key] = theNeta
|
||||
vCLMLog("UOM: Observation finished with existing observation: \(key)")
|
||||
saveCallback?() ?? saveData()
|
||||
vCLog("UOM: Observation finished with existing observation: \(key)")
|
||||
saveCallback()
|
||||
}
|
||||
}
|
||||
|
||||
func getSuggestion(key: String, timestamp: Double, headReading: String) -> LMAssembly.OverrideSuggestion {
|
||||
func getSuggestion(key: String, timestamp: Double, headReading: String) -> Suggestion {
|
||||
guard !key.isEmpty, let kvPair = mutLRUMap[key] else { return .init() }
|
||||
let observation: Observation = kvPair.observation
|
||||
var candidates: [(String, Megrez.Unigram)] = .init()
|
||||
|
@ -399,10 +386,3 @@ extension LMAssembly.LMUserOverride {
|
|||
return result
|
||||
}
|
||||
}
|
||||
|
||||
struct UOMError: LocalizedError {
|
||||
var rawValue: String
|
||||
var errorDescription: String? {
|
||||
NSLocalizedString("rawValue", comment: "")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
// 下述詞頻資料取自 libTaBE 資料庫 (http://sourceforge.net/projects/libtabe/)
|
||||
// (2002 最終版). 該專案於 1999 年由 Pai-Hsiang Hsiao 發起、以 BSD 授權發行。
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by ShikiSuen on 2023/11/26.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
@ -25,8 +29,6 @@ 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);
|
||||
|
|
|
@ -7,9 +7,10 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import Foundation
|
||||
import Shared
|
||||
import SQLite3
|
||||
|
||||
public enum LMAssembly {
|
||||
public enum vChewingLM {
|
||||
enum FileErrors: Error {
|
||||
case fileHandleError(String)
|
||||
}
|
||||
|
@ -55,7 +56,7 @@ extension Array where Element == String {
|
|||
sqlite3_prepare_v2(ptrDB, strStmt, -1, &ptrStmt, nil) == SQLITE_OK && sqlite3_step(ptrStmt) == SQLITE_DONE
|
||||
}
|
||||
guard thisResult else {
|
||||
vCLMLog("SQL Query Error. Statement: \(strStmt)")
|
||||
vCLog("SQL Query Error. Statement: \(strStmt)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -82,13 +83,3 @@ 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)
|
||||
}
|
||||
}
|
|
@ -57,8 +57,8 @@ final class InputTokenTests: XCTestCase {
|
|||
}
|
||||
|
||||
func testGeneratedResultsFromLMInstantiator() throws {
|
||||
let instance = LMAssembly.LMInstantiator(isCHS: true)
|
||||
XCTAssertTrue(LMAssembly.LMInstantiator.connectToTestSQLDB())
|
||||
let instance = vChewingLM.LMInstantiator(isCHS: true)
|
||||
XCTAssertTrue(vChewingLM.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)
|
||||
LMAssembly.LMInstantiator.disconnectSQLDB()
|
||||
vChewingLM.LMInstantiator.disconnectSQLDB()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = LMAssembly.LMCassette()
|
||||
var lmCassette = vChewingLM.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 = LMAssembly.LMCassette()
|
||||
var lmCassette = vChewingLM.LMCassette()
|
||||
NSLog("LMCassette: Start loading CIN.")
|
||||
lmCassette.open(pathCINFile)
|
||||
NSLog("LMCassette: Finished loading CIN. Entries: \(lmCassette.count)")
|
||||
|
|
|
@ -38,7 +38,7 @@ private let sampleData: String = #"""
|
|||
|
||||
final class LMCoreEXTests: XCTestCase {
|
||||
func testLMCoreEXAsFactoryCoreDict() throws {
|
||||
var lmTest = LMAssembly.LMCoreEX(
|
||||
var lmTest = vChewingLM.LMCoreEX(
|
||||
reverse: false, consolidate: false, defaultScore: 0, forceDefaultScore: false
|
||||
)
|
||||
lmTest.replaceData(textData: sampleData)
|
||||
|
|
|
@ -22,8 +22,8 @@ private let expectedReverseLookupResults: [String] = [
|
|||
|
||||
final class LMInstantiatorSQLTests: XCTestCase {
|
||||
func testSQL() throws {
|
||||
let instance = LMAssembly.LMInstantiator(isCHS: true)
|
||||
XCTAssertTrue(LMAssembly.LMInstantiator.connectToTestSQLDB())
|
||||
let instance = vChewingLM.LMInstantiator(isCHS: true)
|
||||
XCTAssertTrue(vChewingLM.LMInstantiator.connectToTestSQLDB())
|
||||
instance.setOptions { config in
|
||||
config.isCNSEnabled = false
|
||||
config.isSymbolEnabled = false
|
||||
|
@ -41,24 +41,7 @@ final class LMInstantiatorSQLTests: XCTestCase {
|
|||
XCTAssertEqual(instance.unigramsFor(keyArray: strRefutationKey).count, 10)
|
||||
XCTAssertEqual(instance.unigramsFor(keyArray: strBoobsKey).last?.description, "(☉☉,-13.0)")
|
||||
// 再測試反查。
|
||||
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)]")
|
||||
XCTAssertEqual(vChewingLM.LMInstantiator.getFactoryReverseLookupData(with: "和"), expectedReverseLookupResults)
|
||||
vChewingLM.LMInstantiator.disconnectSQLDB()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
//// (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, ["节", "洁", "杰"])
|
||||
}
|
||||
}
|
|
@ -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: LMAssembly.LMUserOverride, key: String, candidate: String, timestamp stamp: Double) {
|
||||
private func observe(who uom: vChewingLM.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 = LMAssembly.LMUserOverride(capacity: capacity, decayConstant: Double(halfLife), dataURL: nullURL)
|
||||
let uom = vChewingLM.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 = LMAssembly.LMUserOverride(capacity: capacity, decayConstant: Double(halfLife), dataURL: nullURL)
|
||||
let uom = vChewingLM.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 = LMAssembly.LMUserOverride(capacity: 2, decayConstant: Double(halfLife), dataURL: nullURL)
|
||||
let uom = vChewingLM.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)
|
||||
|
|
|
@ -13,7 +13,7 @@ import XCTest
|
|||
|
||||
final class LMInstantiatorNumericPadTests: XCTestCase {
|
||||
func testSQL() throws {
|
||||
let instance = LMAssembly.LMInstantiator(isCHS: true)
|
||||
let instance = vChewingLM.LMInstantiator(isCHS: true)
|
||||
instance.setOptions { config in
|
||||
config.numPadFWHWStatus = nil
|
||||
}
|
||||
|
|
|
@ -17,12 +17,10 @@ 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_OSFrameworkImpl"),
|
||||
.package(path: "../vChewing_CocoaExtension"),
|
||||
.package(path: "../vChewing_Hotenka"),
|
||||
.package(path: "../vChewing_IMKUtils"),
|
||||
.package(path: "../vChewing_KimoDataReader"),
|
||||
.package(path: "../vChewing_LangModelAssembly"),
|
||||
.package(path: "../vChewing_Megrez"),
|
||||
.package(path: "../vChewing_NotifierUI"),
|
||||
|
@ -39,14 +37,12 @@ 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: "OSFrameworkImpl", package: "vChewing_OSFrameworkImpl"),
|
||||
.product(name: "CocoaExtension", package: "vChewing_CocoaExtension"),
|
||||
.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"),
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import AppKit
|
||||
import Shared
|
||||
import SwiftUI
|
||||
|
||||
public class CtlAboutUI: NSWindowController, NSWindowDelegate {
|
||||
|
|
|
@ -87,11 +87,7 @@ public class VwrAboutCocoa: NSViewController {
|
|||
)
|
||||
NSView()
|
||||
}
|
||||
var verticalButtonStackSpacing: CGFloat? = 4
|
||||
if #unavailable(macOS 10.10) {
|
||||
verticalButtonStackSpacing = nil
|
||||
}
|
||||
NSStackView.build(.vertical, spacing: verticalButtonStackSpacing, width: 114) {
|
||||
NSStackView.build(.vertical, width: 114) {
|
||||
addKeyEquivalent(
|
||||
NSButton(
|
||||
"i18n:aboutWindow.OK_BUTTON",
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import AppKit
|
||||
import Shared
|
||||
import SwiftUI
|
||||
|
||||
public struct VwrAboutUI {
|
||||
|
|
|
@ -47,6 +47,7 @@ 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()
|
||||
}
|
||||
|
@ -69,8 +70,6 @@ public extension AppDelegate {
|
|||
|
||||
SpeechSputnik.shared.refreshStatus() // 根據現狀條件決定是否初期化語音引擎。
|
||||
|
||||
CandidateTextService.enableFinalSanityCheck()
|
||||
|
||||
// 一旦發現與使用者半衰模組的觀察行為有關的崩潰標記被開啟:
|
||||
// 如果有開啟 Debug 模式的話,就將既有的半衰記憶資料檔案更名+打上當時的時間戳。
|
||||
// 如果沒有開啟 Debug 模式的話,則將半衰記憶資料直接清空。
|
||||
|
@ -147,7 +146,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 1024...:
|
||||
case 384...:
|
||||
vCLog("WARNING: EXCESSIVE MEMORY FOOTPRINT (\(currentMemorySize)MB).")
|
||||
let msgPackage = UNMutableNotificationContent()
|
||||
msgPackage.title = NSLocalizedString("vChewing", comment: "")
|
||||
|
@ -169,10 +168,4 @@ public extension AppDelegate {
|
|||
}
|
||||
return currentMemorySize
|
||||
}
|
||||
|
||||
// New About Window
|
||||
@IBAction func about(_: Any) {
|
||||
CtlAboutUI.show()
|
||||
NSApp.popup()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,170 +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 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
|
||||
}
|
||||
}
|
|
@ -7,7 +7,6 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import Hotenka
|
||||
import Shared
|
||||
|
||||
public enum ChineseConverter {
|
||||
public static let shared = HotenkaChineseConverter(
|
||||
|
|
|
@ -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: tableHeight)
|
||||
.makeSimpleConstraint(.height, relation: .equal, value: 232)
|
||||
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, spacing: 6) {
|
||||
NSStackView.build(.vertical) {
|
||||
btnAddClient
|
||||
.makeSimpleConstraint(.width, relation: .equal, value: buttonWidth)
|
||||
btnRemoveClient
|
||||
|
@ -59,8 +59,6 @@ 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
|
||||
}
|
||||
|
@ -74,12 +72,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: tableHeight)
|
||||
tblClients.frame = CGRect(x: 0, y: 0, width: 728, height: 230)
|
||||
tblClients.gridColor = NSColor.clear
|
||||
tblClients.intercellSpacing = CGSize(width: 17, height: 0)
|
||||
tblClients.rowHeight = 24
|
||||
tblClients.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
||||
tblClients.registerForDraggedTypes([.kUTTypeFileURL])
|
||||
tblClients.registerForDraggedTypes([.init(rawValue: kUTTypeFileURL as String)])
|
||||
tblClients.dataSource = self
|
||||
tblClients.action = #selector(onItemClicked(_:))
|
||||
tblClients.target = self
|
||||
|
@ -152,7 +150,7 @@ extension VwrClientListMgr {
|
|||
neta info: NSDraggingInfo, onError: @escaping () -> Void?, handler: (([URL]) -> Void)? = nil
|
||||
) {
|
||||
let board = info.draggingPasteboard
|
||||
let type = NSPasteboard.PasteboardType.kUTTypeAppBundle
|
||||
let type = NSPasteboard.PasteboardType(rawValue: kUTTypeApplicationBundle as String)
|
||||
let options: [NSPasteboard.ReadingOptionKey: Any] = [
|
||||
.urlReadingFileURLsOnly: true,
|
||||
.urlReadingContentsConformToTypes: [type],
|
||||
|
@ -198,7 +196,7 @@ extension VwrClientListMgr {
|
|||
}
|
||||
|
||||
@IBAction func btnAddClientClicked(_: Any) {
|
||||
guard let window = CtlClientListMgr.shared?.window else { return }
|
||||
guard let window = NSApp.keyWindow else { return }
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString(
|
||||
"Please enter the client app bundle identifier(s) you want to register.", comment: ""
|
||||
|
@ -279,11 +277,11 @@ extension VwrClientListMgr {
|
|||
)
|
||||
let text = url.path + "\n\n" + NSLocalizedString("Please try again.", comment: "")
|
||||
guard let bundle = Bundle(url: url) else {
|
||||
CtlClientListMgr.shared?.window.callAlert(title: title, text: text)
|
||||
NSApp.keyWindow?.callAlert(title: title, text: text)
|
||||
return
|
||||
}
|
||||
guard let identifier = bundle.bundleIdentifier else {
|
||||
CtlClientListMgr.shared?.window.callAlert(title: title, text: text)
|
||||
NSApp.keyWindow?.callAlert(title: title, text: text)
|
||||
return
|
||||
}
|
||||
let isIdentifierAlreadyRegistered = Self.clientsList.contains(identifier)
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import AppKit
|
||||
import Carbon
|
||||
import Shared
|
||||
|
||||
// MARK: - Top-level Enums relating to Input Mode and Language Supports.
|
||||
|
||||
|
@ -51,7 +52,7 @@ public enum IMEApp {
|
|||
// MARK: - 輸入法的當前的簡繁體中文模式
|
||||
|
||||
public static var currentInputMode: Shared.InputMode {
|
||||
.init(rawValue: PrefMgr().mostRecentInputMode) ?? .imeModeNULL
|
||||
.init(rawValue: PrefMgr.shared.mostRecentInputMode) ?? .imeModeNULL
|
||||
}
|
||||
|
||||
/// 當前鍵盤是否是 JIS 佈局
|
||||
|
@ -61,10 +62,9 @@ public enum IMEApp {
|
|||
|
||||
/// Fart or Beep?
|
||||
public static func buzz() {
|
||||
let prefs = PrefMgr()
|
||||
if prefs.isDebugModeEnabled {
|
||||
NSSound.buzz(fart: !prefs.shouldNotFartInLieuOfBeep)
|
||||
} else if !prefs.shouldNotFartInLieuOfBeep {
|
||||
if PrefMgr.shared.isDebugModeEnabled {
|
||||
NSSound.buzz(fart: !PrefMgr.shared.shouldNotFartInLieuOfBeep)
|
||||
} else if !PrefMgr.shared.shouldNotFartInLieuOfBeep {
|
||||
NSSound.buzz(fart: true)
|
||||
} else {
|
||||
NSSound.beep()
|
|
@ -200,8 +200,7 @@ 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 || node.containsCandidateServices:
|
||||
return data.attributedStringPlaceholder(for: session)
|
||||
case .ofSymbolTable where displayedText.isEmpty: return data.attributedStringPlaceholder(for: session)
|
||||
case .ofSymbolTable where !displayedText.isEmpty: break
|
||||
default: break
|
||||
}
|
||||
|
|
|
@ -212,7 +212,7 @@ public extension IMEStateData {
|
|||
subNeta = Tekkon.cnvPhonaToHanyuPinyin(targetJoined: subNeta)
|
||||
subNeta = Tekkon.cnvHanyuPinyinToTextbookStyle(targetJoined: subNeta)
|
||||
} else {
|
||||
subNeta = Tekkon.cnvPhonaToTextbookStyle(target: subNeta)
|
||||
subNeta = Tekkon.cnvPhonaToTextbookReading(target: subNeta)
|
||||
}
|
||||
}
|
||||
arrOutput.append(subNeta)
|
||||
|
|
|
@ -19,7 +19,8 @@ import Tekkon
|
|||
// MARK: - InputHandler 自身協定 (Protocol).
|
||||
|
||||
public protocol InputHandlerProtocol {
|
||||
var currentLM: LMAssembly.LMInstantiator { get set }
|
||||
var currentLM: vChewingLM.LMInstantiator { get set }
|
||||
var currentUOM: vChewingLM.LMUserOverride { get set }
|
||||
var delegate: InputHandlerDelegate? { get set }
|
||||
var keySeparator: String { get }
|
||||
static var keySeparator: String { get }
|
||||
|
@ -98,7 +99,8 @@ public class InputHandler: InputHandlerProtocol {
|
|||
var composer: Tekkon.Composer = .init() // 注拼槽
|
||||
var compositor: Megrez.Compositor // 組字器
|
||||
|
||||
public var currentLM: LMAssembly.LMInstantiator {
|
||||
public var currentUOM: vChewingLM.LMUserOverride
|
||||
public var currentLM: vChewingLM.LMInstantiator {
|
||||
didSet {
|
||||
compositor.langModel = .init(withLM: currentLM)
|
||||
clear()
|
||||
|
@ -106,9 +108,10 @@ public class InputHandler: InputHandlerProtocol {
|
|||
}
|
||||
|
||||
/// 初期化。
|
||||
public init(lm: LMAssembly.LMInstantiator, pref: PrefMgrProtocol) {
|
||||
public init(lm: vChewingLM.LMInstantiator, uom: vChewingLM.LMUserOverride, pref: PrefMgrProtocol) {
|
||||
prefs = pref
|
||||
currentLM = lm
|
||||
currentUOM = uom
|
||||
/// 同步組字器單個詞的幅位長度上限。
|
||||
Megrez.Compositor.maxSpanLength = prefs.maxCandidateLength
|
||||
/// 組字器初期化。因為是首次初期化變數,所以這裡不能用 ensureCompositor() 代勞。
|
||||
|
@ -366,8 +369,8 @@ public class InputHandler: InputHandlerProtocol {
|
|||
let currentNode = currentWalk.findNode(at: actualNodeCursorPosition, target: &accumulatedCursor)
|
||||
guard let currentNode = currentNode else { return }
|
||||
|
||||
uomProcessing: if currentNode.currentUnigram.score > -12, prefs.fetchSuggestionsFromUserOverrideModel {
|
||||
if skipObservation { break uomProcessing }
|
||||
uom: if currentNode.currentUnigram.score > -12, prefs.fetchSuggestionsFromUserOverrideModel {
|
||||
if skipObservation { break uom }
|
||||
vCLog("UOM: Start Observation.")
|
||||
// 這個過程可能會因為使用者半衰記憶模組內部資料錯亂、而導致輸入法在選字時崩潰。
|
||||
// 於是在這裡引入災後狀況察覺專用變數,且先開啟該開關。順利執行完觀察後會關閉。
|
||||
|
@ -375,9 +378,9 @@ public class InputHandler: InputHandlerProtocol {
|
|||
prefs.failureFlagForUOMObservation = true
|
||||
// 令半衰記憶模組觀測給定的三元圖。
|
||||
// 這個過程會讓半衰引擎根據當前上下文生成三元圖索引鍵。
|
||||
currentLM.performUOMObservation(
|
||||
currentUOM.performObservation(
|
||||
walkedBefore: previousWalk, walkedAfter: currentWalk, cursor: actualNodeCursorPosition,
|
||||
timestamp: Date().timeIntervalSince1970
|
||||
timestamp: Date().timeIntervalSince1970, saveCallback: { self.currentUOM.saveData() }
|
||||
)
|
||||
// 如果沒有出現崩框的話,那就將這個開關復位。
|
||||
prefs.failureFlagForUOMObservation = false
|
||||
|
@ -429,7 +432,7 @@ public class InputHandler: InputHandlerProtocol {
|
|||
/// 如果這個開關沒打開的話,直接放棄執行這個函式。
|
||||
if !prefs.fetchSuggestionsFromUserOverrideModel { return arrResult }
|
||||
/// 獲取來自半衰記憶模組的建議結果
|
||||
let suggestion = currentLM.fetchUOMSuggestion(
|
||||
let suggestion = currentUOM.fetchSuggestion(
|
||||
currentWalk: compositor.walkedNodes, cursor: actualNodeCursorPosition, timestamp: Date().timeIntervalSince1970
|
||||
)
|
||||
arrResult.append(contentsOf: suggestion.candidates)
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
/// 該檔案乃輸入調度模組當中「用來規定在選字窗出現時的按鍵行為」的部分。
|
||||
|
||||
import CandidateWindow
|
||||
import CocoaExtension
|
||||
import InputMethodKit
|
||||
import Megrez
|
||||
import OSFrameworkImpl
|
||||
import Shared
|
||||
|
||||
// MARK: - § 對選字狀態進行調度 (Handle Candidate State).
|
||||
|
@ -30,47 +30,6 @@ 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: 選字窗內使用熱鍵升權、降權、刪詞。
|
||||
|
||||
|
@ -142,6 +101,7 @@ 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()
|
||||
|
@ -355,7 +315,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]
|
||||
|
@ -373,6 +333,24 @@ 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")
|
||||
|
|
|
@ -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(pronounceableOnly: prefs.acceptLeadingIntonations)
|
||||
let maybeKey = maybeKey ?? composer.phonabetKeyForQuery(pronouncable: 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(pronounceableOnly: prefs.acceptLeadingIntonations)
|
||||
let maybeKey = composer.phonabetKeyForQuery(pronouncable: 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(pronounceableOnly: false) == nil {
|
||||
if composer.phonabetKeyForQuery(pronouncable: false) == nil {
|
||||
// 將被空格鍵覆蓋掉的既有聲調塞入組字器。
|
||||
if !composer.isPinyinMode, input.isSpace,
|
||||
compositor.insertKey(existedIntonation.value)
|
||||
|
@ -422,9 +422,16 @@ 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: IMEApp.currentInputMode.nonUTFEncoding)?.first?.description
|
||||
.parsedAsHexLiteral(encoding: encoding)?.first?.description
|
||||
else {
|
||||
delegate.callError("D220B880:輸入的字碼沒有對應的字元。")
|
||||
var updatedState = IMEState.ofAbortion()
|
||||
|
|
|
@ -393,6 +393,75 @@ 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) 按鍵行為。
|
||||
|
@ -898,20 +967,6 @@ 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 與英數輸入模式。
|
||||
|
@ -1041,15 +1096,11 @@ extension InputHandler {
|
|||
let fullWidthResult = behaviorValue % 2 != 0 // 能被二整除的都是半形。
|
||||
triagePrefs: switch (behaviorValue, isConsideredEmptyForNow) {
|
||||
case (2, _), (3, _), (4, false), (5, false):
|
||||
currentLM.setOptions { config in
|
||||
config.numPadFWHWStatus = fullWidthResult
|
||||
}
|
||||
currentLM.config.numPadFWHWStatus = fullWidthResult
|
||||
if handlePunctuation("_NumPad_\(inputText)") { return true }
|
||||
default: break triagePrefs // 包括 case 0 & 1。
|
||||
}
|
||||
currentLM.setOptions { config in
|
||||
config.numPadFWHWStatus = nil
|
||||
}
|
||||
currentLM.config.numPadFWHWStatus = nil
|
||||
delegate.switchState(IMEState.ofEmpty())
|
||||
let charToCommit = inputText.applyingTransformFW2HW(reverse: fullWidthResult)
|
||||
delegate.switchState(IMEState.ofCommitting(textToCommit: charToCommit))
|
||||
|
|
|
@ -1,125 +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 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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import Foundation
|
||||
import Shared
|
||||
import SwiftExtension
|
||||
|
||||
// MARK: - Typing Method
|
||||
|
@ -31,8 +30,12 @@ public extension InputHandler {
|
|||
case .codePoint:
|
||||
let commonTerm = NSMutableString()
|
||||
commonTerm.insert("Code Point Input.".localized, at: 0)
|
||||
if !vertical, let initials = IMEApp.currentInputMode.nonUTFEncodingInitials {
|
||||
commonTerm.insert("[\(initials)] ", at: 0)
|
||||
if !vertical {
|
||||
switch IMEApp.currentInputMode {
|
||||
case .imeModeCHS: commonTerm.insert("[GB] ", at: 0)
|
||||
case .imeModeCHT: commonTerm.insert("[Big5] ", at: 0)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
return commonTerm.description
|
||||
case .haninKeyboardSymbol:
|
||||
|
|
|
@ -15,20 +15,26 @@ import SwiftExtension
|
|||
// MARK: - Input Mode Extension for Language Models
|
||||
|
||||
public extension Shared.InputMode {
|
||||
private static let lmCHS = LMAssembly.LMInstantiator(
|
||||
isCHS: true, uomDataURL: LMMgr.userOverrideModelDataURL(.imeModeCHS)
|
||||
)
|
||||
private static let lmCHT = LMAssembly.LMInstantiator(
|
||||
isCHS: false, uomDataURL: LMMgr.userOverrideModelDataURL(.imeModeCHT)
|
||||
)
|
||||
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))
|
||||
|
||||
var langModel: LMAssembly.LMInstantiator {
|
||||
var langModel: vChewingLM.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.
|
||||
|
@ -48,14 +54,14 @@ public class LMMgr {
|
|||
Self.loadUserPhrasesData()
|
||||
}
|
||||
|
||||
public static var isCoreDBConnected: Bool { LMAssembly.LMInstantiator.isSQLDBConnected }
|
||||
public static var isCoreDBConnected: Bool { vChewingLM.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 = LMAssembly.LMInstantiator.connectSQLDB(dbPath: path)
|
||||
let result = vChewingLM.LMInstantiator.connectSQLDB(dbPath: path)
|
||||
assert(result, "vChewing factory SQLite connection failed.")
|
||||
Notifier.notify(
|
||||
message: NSLocalizedString("Core Dict loading complete.", comment: "")
|
||||
|
@ -65,15 +71,10 @@ public class LMMgr {
|
|||
/// 載入磁帶資料。
|
||||
/// - Remark: cassettePath() 會在輸入法停用磁帶時直接返回
|
||||
public static func loadCassetteData() {
|
||||
func validateCassetteCandidateKey(_ target: String) -> Bool {
|
||||
CandidateKey.validate(keys: target) == nil
|
||||
}
|
||||
|
||||
LMAssembly.LMInstantiator.setCassetCandidateKeyValidator(validateCassetteCandidateKey)
|
||||
LMAssembly.LMInstantiator.loadCassetteData(path: cassettePath())
|
||||
vChewingLM.LMInstantiator.loadCassetteData(path: cassettePath())
|
||||
}
|
||||
|
||||
public static func loadUserPhrasesData(type: LMAssembly.ReplacableUserDataType? = nil) {
|
||||
public static func loadUserPhrasesData(type: vChewingLM.ReplacableUserDataType? = nil) {
|
||||
guard let type = type else {
|
||||
Shared.InputMode.validCases.forEach { mode in
|
||||
mode.langModel.loadUserPhrasesData(
|
||||
|
@ -81,11 +82,12 @@ public class LMMgr {
|
|||
filterPath: userDictDataURL(mode: mode, type: .theFilter).path
|
||||
)
|
||||
mode.langModel.loadUserSymbolData(path: userDictDataURL(mode: mode, type: .theSymbols).path)
|
||||
mode.langModel.loadUOMData()
|
||||
mode.uom.loadData(fromURL: userOverrideModelDataURL(mode))
|
||||
}
|
||||
|
||||
if PrefMgr.shared.associatedPhrasesEnabled { Self.loadUserAssociatesData() }
|
||||
if PrefMgr.shared.phraseReplacementEnabled { Self.loadUserPhraseReplacement() }
|
||||
if PrefMgr.shared.useSCPCTypingMode { Self.loadSCPCSequencesData() }
|
||||
|
||||
CandidateNode.load(url: Self.userSymbolMenuDataURL())
|
||||
return
|
||||
|
@ -129,6 +131,12 @@ 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)
|
||||
}
|
||||
|
@ -170,7 +178,6 @@ 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
|
||||
}
|
||||
}
|
||||
|
@ -179,12 +186,12 @@ public class LMMgr {
|
|||
// MARK: UOM
|
||||
|
||||
public static func saveUserOverrideModelData() {
|
||||
let globalQueue = DispatchQueue(label: "LMAssembly_UOM", qos: .unspecified, attributes: .concurrent)
|
||||
let globalQueue = DispatchQueue(label: "vChewingLM_UOM", qos: .unspecified, attributes: .concurrent)
|
||||
let group = DispatchGroup()
|
||||
Shared.InputMode.validCases.forEach { mode in
|
||||
group.enter()
|
||||
globalQueue.async {
|
||||
mode.langModel.saveUOMData()
|
||||
mode.uom.saveData(toURL: userOverrideModelDataURL(mode))
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
@ -193,11 +200,11 @@ public class LMMgr {
|
|||
}
|
||||
|
||||
public static func bleachSpecifiedSuggestions(targets: [String], mode: Shared.InputMode) {
|
||||
mode.langModel.bleachSpecifiedUOMSuggestions(targets: targets)
|
||||
mode.uom.bleachSpecifiedSuggestions(targets: targets, saveCallback: { mode.uom.saveData() })
|
||||
}
|
||||
|
||||
public static func removeUnigramsFromUserOverrideModel(_ mode: Shared.InputMode) {
|
||||
mode.langModel.bleachUOMUnigrams()
|
||||
mode.uom.bleachUnigrams(saveCallback: { mode.uom.saveData() })
|
||||
}
|
||||
|
||||
public static func relocateWreckedUOMData() {
|
||||
|
@ -219,6 +226,6 @@ public class LMMgr {
|
|||
}
|
||||
|
||||
public static func clearUserOverrideModelData(_ mode: Shared.InputMode = .imeModeNULL) {
|
||||
mode.langModel.clearUOMData()
|
||||
mode.uom.clearData(withURL: userOverrideModelDataURL(mode))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,78 +6,15 @@
|
|||
// 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) throws -> Int {
|
||||
@discardableResult static func importYahooKeyKeyUserDictionary(text rawString: inout String) -> 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 }
|
||||
|
@ -87,25 +24,12 @@ 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 }
|
||||
|
||||
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 allPhrasesCHS = allPhrasesCHT.compactMap { chtPhrase in
|
||||
let chsPhrase = chtPhrase.crossConverted
|
||||
return chsPhrase.isValid && !chsPhrase.isDuplicated ? chsPhrase : nil
|
||||
}.deduplicated
|
||||
let outputStrCHS = allPhrasesCHS.map(\.description).joined(separator: "\n")
|
||||
let outputStrCHT = allPhrasesCHT.map(\.description).joined(separator: "\n")
|
||||
var outputDataCHS = "\(outputStrCHS)\n".data(using: .utf8) ?? .init([])
|
||||
|
@ -115,7 +39,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 false }
|
||||
guard let fileHandlerCHS = fileHandlerCHS, let fileHandlerCHT = fileHandlerCHT else { return 0 }
|
||||
defer {
|
||||
fileHandlerCHS.closeFile()
|
||||
fileHandlerCHT.closeFile()
|
||||
|
@ -138,7 +62,8 @@ public extension LMMgr {
|
|||
}
|
||||
fileHandlerCHT.seekToEndOfFile()
|
||||
fileHandlerCHT.write(outputDataCHT)
|
||||
return true
|
||||
|
||||
return allPhrasesCHT.count
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,23 +17,23 @@ import Shared
|
|||
extension LMMgr: PhraseEditorDelegate {
|
||||
public var currentInputMode: Shared.InputMode { IMEApp.currentInputMode }
|
||||
|
||||
public func openPhraseFile(mode: Shared.InputMode, type: LMAssembly.ReplacableUserDataType, using app: FileOpenMethod) {
|
||||
public func openPhraseFile(mode: Shared.InputMode, type: vChewingLM.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) {
|
||||
LMAssembly.LMConsolidator.consolidate(text: &strProcessed, pragma: shouldCheckPragma)
|
||||
vChewingLM.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: LMAssembly.ReplacableUserDataType) -> String {
|
||||
public func retrieveData(mode: Shared.InputMode, type: vChewingLM.ReplacableUserDataType) -> String {
|
||||
Self.retrieveData(mode: mode, type: type)
|
||||
}
|
||||
|
||||
public static func retrieveData(mode: Shared.InputMode, type: LMAssembly.ReplacableUserDataType) -> String {
|
||||
public static func retrieveData(mode: Shared.InputMode, type: vChewingLM.ReplacableUserDataType) -> String {
|
||||
vCLog("Retrieving data. Mode: \(mode.localizedDescription), type: \(type.localizedDescription)")
|
||||
let theURL = Self.userDictDataURL(mode: mode, type: type)
|
||||
do {
|
||||
|
@ -44,12 +44,12 @@ extension LMMgr: PhraseEditorDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
public func saveData(mode: Shared.InputMode, type: LMAssembly.ReplacableUserDataType, data: String) -> String {
|
||||
public func saveData(mode: Shared.InputMode, type: vChewingLM.ReplacableUserDataType, data: String) -> String {
|
||||
Self.saveData(mode: mode, type: type, data: data)
|
||||
}
|
||||
|
||||
@discardableResult public static func saveData(
|
||||
mode: Shared.InputMode, type: LMAssembly.ReplacableUserDataType, data: String
|
||||
mode: Shared.InputMode, type: vChewingLM.ReplacableUserDataType, data: String
|
||||
) -> String {
|
||||
DispatchQueue.main.async {
|
||||
let theURL = Self.userDictDataURL(mode: mode, type: type)
|
||||
|
|
|
@ -6,10 +6,8 @@
|
|||
// 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: - 使用者語彙類型定義
|
||||
|
@ -34,10 +32,6 @@ 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: " ")
|
||||
}
|
||||
|
@ -94,7 +88,7 @@ public extension LMMgr {
|
|||
/// 有些使用者的語彙檔案已經過於龐大了(超過一千行),
|
||||
/// 每次寫入時都全文整理格式的話,會引發嚴重的效能問題。
|
||||
/// 所以這裡不再強制要求整理格式。
|
||||
let theType: LMAssembly.ReplacableUserDataType = toFilter ? .theFilter : .thePhrases
|
||||
let theType: vChewingLM.ReplacableUserDataType = toFilter ? .theFilter : .thePhrases
|
||||
let theURL = LMMgr.userDictDataURL(mode: inputMode, type: theType)
|
||||
var fileSize: UInt64?
|
||||
do {
|
||||
|
@ -149,7 +143,7 @@ public extension LMMgr {
|
|||
}
|
||||
}
|
||||
let theURL = LMMgr.userDictDataURL(mode: inputMode, type: .theFilter)
|
||||
if forceConsolidate, !LMAssembly.LMConsolidator.consolidate(path: theURL.path, pragma: false) { return false }
|
||||
if forceConsolidate, !vChewingLM.LMConsolidator.consolidate(path: theURL.path, pragma: false) { return false }
|
||||
// Get FileSize.
|
||||
var fileSize: UInt64?
|
||||
do {
|
||||
|
@ -196,51 +190,3 @@ 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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@ public extension LMMgr {
|
|||
/// - mode: 繁簡模式。
|
||||
/// - type: 辭典資料類型
|
||||
/// - Returns: 資料路徑(URL)。
|
||||
static func userDictDataURL(mode: Shared.InputMode, type: LMAssembly.ReplacableUserDataType) -> URL {
|
||||
static func userDictDataURL(mode: Shared.InputMode, type: vChewingLM.ReplacableUserDataType) -> URL {
|
||||
var fileName: String = {
|
||||
switch type {
|
||||
case .thePhrases: return "userdata"
|
||||
|
@ -240,6 +240,7 @@ 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()
|
||||
}
|
||||
|
@ -271,7 +272,7 @@ public extension LMMgr {
|
|||
return true
|
||||
}
|
||||
|
||||
static func openUserDictFile(type: LMAssembly.ReplacableUserDataType, dual: Bool = false, alt: Bool) {
|
||||
static func openUserDictFile(type: vChewingLM.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 }
|
||||
|
@ -324,7 +325,7 @@ public extension LMMgr {
|
|||
/// 前者的話,需要該檔案存在的人自己會建立。
|
||||
/// 後者的話,你在敲字時自己就會建立。
|
||||
var failed = false
|
||||
caseCheck: for type in LMAssembly.ReplacableUserDataType.allCases {
|
||||
caseCheck: for type in vChewingLM.ReplacableUserDataType.allCases {
|
||||
let templateName = Self.templateName(for: type, mode: mode)
|
||||
if !ensureFileExists(userDictDataURL(mode: mode, type: type), deployTemplate: templateName) {
|
||||
failed = true
|
||||
|
@ -334,7 +335,7 @@ public extension LMMgr {
|
|||
return !failed
|
||||
}
|
||||
|
||||
internal static func templateName(for type: LMAssembly.ReplacableUserDataType, mode: Shared.InputMode) -> String {
|
||||
internal static func templateName(for type: vChewingLM.ReplacableUserDataType, mode: Shared.InputMode) -> String {
|
||||
switch type {
|
||||
case .thePhrases: return kTemplateNameUserPhrases
|
||||
case .theFilter: return kTemplateNameUserFilterList
|
||||
|
@ -345,26 +346,3 @@ 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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,12 +6,14 @@
|
|||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
import Shared
|
||||
import SwiftExtension
|
||||
|
||||
// MARK: -
|
||||
|
||||
@objcMembers public class PrefMgr: NSObject, PrefMgrProtocol {
|
||||
public static let shared = PrefMgr()
|
||||
public static let kDefaultCandidateKeys = "123456"
|
||||
public static let kDefaultBasicKeyboardLayout = "com.apple.keylayout.ZhuyinBopomofo"
|
||||
public static let kDefaultAlphanumericalKeyboardLayout = {
|
||||
|
@ -25,33 +27,6 @@ import SwiftExtension
|
|||
"com.valvesoftware.steam": true, "jp.naver.line.mac": true,
|
||||
]
|
||||
|
||||
public static let kDefaultCandidateServiceMenuItem: [String] = [
|
||||
#"Unicode Metadata: %s"# + "\t" + #"@SEL:copyUnicodeMetadata:"#,
|
||||
#"macOS Dict: %s"# + "\t" + #"@URL:dict://%s"#,
|
||||
#"Bing: %s"# + "\t" + #"@WEB:https://www.bing.com/search?q=%s"#,
|
||||
#"DuckDuckGo: %s"# + "\t" + #"@WEB:https://duckduckgo.com/?t=h_&q=%s"#,
|
||||
#"Ecosia: %s"# + "\t" + #"@WEB:https://www.ecosia.org/search?method=index&q=%s"#,
|
||||
#"Google: %s"# + "\t" + #"@WEB:https://www.google.com/search?q=%s"#,
|
||||
#"MoeDict: %s"# + "\t" + #"@WEB:https://www.moedict.tw/%s"#,
|
||||
#"Wikitonary: %s"# + "\t" + #"@WEB:https://zh.wiktionary.org/wiki/Special:Search?search=%s"#,
|
||||
#"Unihan: %s"# + "\t" + #"@WEB:https://www.unicode.org/cgi-bin/GetUnihanData.pl?codepoint=%s"#,
|
||||
#"Zi-Hi: %s"# + "\t" + #"@WEB:https://zi-hi.com/sp/uni/%s"#,
|
||||
#"HTML Ruby Zhuyin: %s"# + "\t" + #"@SEL:copyRubyHTMLZhuyinTextbookStyle:"#,
|
||||
#"HTML Ruby Pinyin: %s"# + "\t" + #"@SEL:copyRubyHTMLHanyuPinyinTextbookStyle:"#,
|
||||
#"Zhuyin Annotation: %s"# + "\t" + #"@SEL:copyInlineZhuyinAnnotationTextbookStyle:"#,
|
||||
#"Pinyin Annotation: %s"# + "\t" + #"@SEL:copyInlineHanyuPinyinAnnotationTextbookStyle:"#,
|
||||
#"Braille 1947: %s"# + "\t" + #"@SEL:copyBraille1947:"#,
|
||||
#"Braille 2018: %s"# + "\t" + #"@SEL:copyBraille2018:"#,
|
||||
#"Baidu: %s"# + "\t" + #"@WEB:https://www.baidu.com/s?wd=%s"#,
|
||||
#"BiliBili: %s"# + "\t" + #"@WEB:https://search.bilibili.com/all?keyword=%s"#,
|
||||
#"Genshin BiliWiki: %s"# + "\t" + #"@WEB:https://wiki.biligame.com/ys/%s"#,
|
||||
#"HSR BiliWiki: %s"# + "\t" + #"@WEB:https://wiki.biligame.com/sr/%s"#,
|
||||
]
|
||||
|
||||
public var didAskForSyncingLMPrefs: (() -> Void)?
|
||||
public var didAskForRefreshingSpeechSputnik: (() -> Void)?
|
||||
public var didAskForSyncingShiftKeyDetectorPrefs: (() -> Void)?
|
||||
|
||||
// MARK: - Settings (Tier 1)
|
||||
|
||||
@AppProperty(key: UserDef.kIsDebugModeEnabled.rawValue, defaultValue: false)
|
||||
|
@ -60,9 +35,6 @@ import SwiftExtension
|
|||
@AppProperty(key: UserDef.kFailureFlagForUOMObservation.rawValue, defaultValue: false)
|
||||
public dynamic var failureFlagForUOMObservation: Bool
|
||||
|
||||
@AppProperty(key: UserDef.kCandidateServiceMenuContents.rawValue, defaultValue: kDefaultCandidateServiceMenuItem)
|
||||
public dynamic var candidateServiceMenuContents: [String]
|
||||
|
||||
@AppProperty(key: UserDef.kRespectClientAccentColor.rawValue, defaultValue: true)
|
||||
public dynamic var respectClientAccentColor: Bool
|
||||
|
||||
|
@ -134,9 +106,6 @@ import SwiftExtension
|
|||
@AppProperty(key: UserDef.kUseJKtoMoveCompositorCursorInCandidateState.rawValue, defaultValue: false)
|
||||
public var useJKtoMoveCompositorCursorInCandidateState: Bool
|
||||
|
||||
@AppProperty(key: UserDef.kUseShiftQuestionToCallServiceMenu.rawValue, defaultValue: true)
|
||||
public var useShiftQuestionToCallServiceMenu: Bool
|
||||
|
||||
@AppProperty(key: UserDef.kMoveCursorAfterSelectingCandidate.rawValue, defaultValue: true)
|
||||
public dynamic var moveCursorAfterSelectingCandidate: Bool
|
||||
|
||||
|
@ -149,9 +118,6 @@ import SwiftExtension
|
|||
@AppProperty(key: UserDef.kUseHorizontalCandidateList.rawValue, defaultValue: true)
|
||||
public dynamic var useHorizontalCandidateList: Bool
|
||||
|
||||
@AppProperty(key: UserDef.kMinCellWidthForHorizontalMatrix.rawValue, defaultValue: 0)
|
||||
public dynamic var minCellWidthForHorizontalMatrix: Int
|
||||
|
||||
@AppProperty(key: UserDef.kChooseCandidateUsingSpace.rawValue, defaultValue: true)
|
||||
public dynamic var chooseCandidateUsingSpace: Bool
|
||||
|
||||
|
@ -169,7 +135,9 @@ import SwiftExtension
|
|||
|
||||
@AppProperty(key: UserDef.kReadingNarrationCoverage.rawValue, defaultValue: 0)
|
||||
public dynamic var readingNarrationCoverage: Int {
|
||||
didSet { didAskForRefreshingSpeechSputnik?() }
|
||||
didSet {
|
||||
SpeechSputnik.shared.refreshStatus()
|
||||
}
|
||||
}
|
||||
|
||||
@AppProperty(key: UserDef.kAlsoConfirmAssociatedCandidatesByEnter.rawValue, defaultValue: false)
|
||||
|
@ -192,12 +160,16 @@ import SwiftExtension
|
|||
|
||||
@AppProperty(key: UserDef.kTogglingAlphanumericalModeWithLShift.rawValue, defaultValue: true)
|
||||
public dynamic var togglingAlphanumericalModeWithLShift: Bool {
|
||||
didSet { didAskForSyncingShiftKeyDetectorPrefs?() }
|
||||
didSet {
|
||||
SessionCtl.theShiftKeyDetector.toggleWithLShift = togglingAlphanumericalModeWithLShift
|
||||
}
|
||||
}
|
||||
|
||||
@AppProperty(key: UserDef.kTogglingAlphanumericalModeWithRShift.rawValue, defaultValue: true)
|
||||
public dynamic var togglingAlphanumericalModeWithRShift: Bool {
|
||||
didSet { didAskForSyncingShiftKeyDetectorPrefs?() }
|
||||
didSet {
|
||||
SessionCtl.theShiftKeyDetector.toggleWithRShift = togglingAlphanumericalModeWithRShift
|
||||
}
|
||||
}
|
||||
|
||||
@AppProperty(key: UserDef.kConsolidateContextOnCandidateSelection.rawValue, defaultValue: true)
|
||||
|
@ -224,9 +196,6 @@ import SwiftExtension
|
|||
@AppProperty(key: UserDef.kShowReverseLookupInCandidateUI.rawValue, defaultValue: true)
|
||||
public dynamic var showReverseLookupInCandidateUI: Bool
|
||||
|
||||
@AppProperty(key: UserDef.kShowCodePointInCandidateUI.rawValue, defaultValue: true)
|
||||
public dynamic var showCodePointInCandidateUI: Bool
|
||||
|
||||
@AppProperty(key: UserDef.kAutoCompositeWithLongestPossibleCassetteKey.rawValue, defaultValue: true)
|
||||
public dynamic var autoCompositeWithLongestPossibleCassetteKey: Bool
|
||||
|
||||
|
@ -267,22 +236,25 @@ import SwiftExtension
|
|||
@AppProperty(key: UserDef.kInlineDumpPinyinInLieuOfZhuyin.rawValue, defaultValue: false)
|
||||
public dynamic var inlineDumpPinyinInLieuOfZhuyin: Bool
|
||||
|
||||
@AppProperty(key: UserDef.kFilterNonCNSReadingsForCHTInput.rawValue, defaultValue: false)
|
||||
public dynamic var filterNonCNSReadingsForCHTInput: Bool
|
||||
|
||||
@AppProperty(key: UserDef.kCNS11643Enabled.rawValue, defaultValue: false)
|
||||
public dynamic var cns11643Enabled: Bool {
|
||||
didSet { didAskForSyncingLMPrefs?() }
|
||||
didSet {
|
||||
LMMgr.syncLMPrefs()
|
||||
}
|
||||
}
|
||||
|
||||
@AppProperty(key: UserDef.kSymbolInputEnabled.rawValue, defaultValue: true)
|
||||
public dynamic var symbolInputEnabled: Bool {
|
||||
didSet { didAskForSyncingLMPrefs?() }
|
||||
didSet {
|
||||
LMMgr.syncLMPrefs()
|
||||
}
|
||||
}
|
||||
|
||||
@AppProperty(key: UserDef.kCassetteEnabled.rawValue, defaultValue: false)
|
||||
public dynamic var cassetteEnabled: Bool {
|
||||
didSet { didAskForSyncingLMPrefs?() }
|
||||
didSet {
|
||||
LMMgr.syncLMPrefs()
|
||||
}
|
||||
}
|
||||
|
||||
@AppProperty(key: UserDef.kChineseConversionEnabled.rawValue, defaultValue: false)
|
||||
|
@ -341,9 +313,6 @@ import SwiftExtension
|
|||
@AppProperty(key: UserDef.kSpecifyShiftSpaceKeyBehavior.rawValue, defaultValue: false)
|
||||
public dynamic var specifyShiftSpaceKeyBehavior: Bool
|
||||
|
||||
@AppProperty(key: UserDef.kSpecifyCmdOptCtrlEnterBehavior.rawValue, defaultValue: 0)
|
||||
public dynamic var specifyCmdOptCtrlEnterBehavior: Int
|
||||
|
||||
// MARK: - Optional settings
|
||||
|
||||
@AppProperty(key: UserDef.kCandidateTextFontName.rawValue, defaultValue: "")
|
||||
|
@ -362,17 +331,31 @@ import SwiftExtension
|
|||
|
||||
@AppProperty(key: UserDef.kUseSCPCTypingMode.rawValue, defaultValue: false)
|
||||
public dynamic var useSCPCTypingMode: Bool {
|
||||
didSet { didAskForSyncingLMPrefs?() }
|
||||
willSet {
|
||||
if newValue {
|
||||
LMMgr.loadSCPCSequencesData()
|
||||
LMMgr.syncLMPrefs()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AppProperty(key: UserDef.kPhraseReplacementEnabled.rawValue, defaultValue: false)
|
||||
public dynamic var phraseReplacementEnabled: Bool {
|
||||
didSet { didAskForSyncingLMPrefs?() }
|
||||
willSet {
|
||||
LMMgr.syncLMPrefs()
|
||||
if newValue {
|
||||
LMMgr.loadUserPhraseReplacement()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AppProperty(key: UserDef.kAssociatedPhrasesEnabled.rawValue, defaultValue: false)
|
||||
public dynamic var associatedPhrasesEnabled: Bool {
|
||||
didSet { didAskForSyncingLMPrefs?() }
|
||||
willSet {
|
||||
if newValue {
|
||||
LMMgr.loadUserAssociatesData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Keyboard HotKey Enable / Disable
|
|
@ -7,16 +7,28 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import InputMethodKit
|
||||
import Shared
|
||||
import SwiftExtension
|
||||
import SwiftUI
|
||||
|
||||
// MARK: ObservableProject.
|
||||
|
||||
@available(macOS 10.15, *)
|
||||
extension PrefMgr: ObservableObject {}
|
||||
|
||||
extension PrefMgr {
|
||||
func sendObjWillChange() {
|
||||
if #available(macOS 10.15, *) {
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Guarded Method for Validating Candidate Keys.
|
||||
|
||||
public extension PrefMgr {
|
||||
func validate(candidateKeys: String) -> String? {
|
||||
var excluded = ""
|
||||
if useJKtoMoveCompositorCursorInCandidateState { excluded.append("jk") }
|
||||
if useShiftQuestionToCallServiceMenu { excluded.append("?") }
|
||||
excluded.append(IMEApp.isKeyboardJIS ? "_" : "`~")
|
||||
let excluded = useJKtoMoveCompositorCursorInCandidateState ? "jk" : ""
|
||||
return CandidateKey.validate(keys: candidateKeys, excluding: excluded)
|
||||
}
|
||||
}
|
||||
|
@ -68,9 +80,6 @@ public extension PrefMgr {
|
|||
if ![0, 1, 2].contains(readingNarrationCoverage) {
|
||||
readingNarrationCoverage = 0
|
||||
}
|
||||
if ![0, 1, 2, 3].contains(specifyCmdOptCtrlEnterBehavior) {
|
||||
specifyCmdOptCtrlEnterBehavior = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -78,7 +87,7 @@ public extension PrefMgr {
|
|||
|
||||
public extension PrefMgr {
|
||||
@discardableResult func dumpShellScriptBackup() -> String? {
|
||||
let mirror = Mirror(reflecting: self)
|
||||
let mirror = Mirror(reflecting: PrefMgr.shared)
|
||||
guard let bundleIdentifier = Bundle.main.bundleIdentifier else { return nil }
|
||||
let strDoubleDashLine = String(String(repeating: "=", count: 70))
|
||||
let consoleOutput = NSMutableString(string: "#!/bin/sh\n\n")
|
|
@ -1,36 +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 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -150,7 +150,7 @@ class FrmRevLookupWindow: NSWindow {
|
|||
strBuilder.append("Maximum 15 results returnable.".localized + "\n")
|
||||
break theLoop
|
||||
}
|
||||
let arrResult = LMAssembly.LMInstantiator.getFactoryReverseLookupData(with: char)?.deduplicated ?? []
|
||||
let arrResult = vChewingLM.LMInstantiator.getFactoryReverseLookupData(with: char)?.deduplicated ?? []
|
||||
if !arrResult.isEmpty {
|
||||
strBuilder.append(char + "\t")
|
||||
strBuilder.append(arrResult.joined(separator: ", "))
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue