Compare commits

...

44 Commits
3.8.2 ... main

Author SHA1 Message Date
ShikiSuen abb17e9ff6 [VersionUp] 3.8.5 GM Build 3850. 2024-04-07 00:56:09 +08:00
ShikiSuen afad2076b0 Update Data - 20240407 2024-04-07 00:26:41 +08:00
ShikiSuen 40245cf1d5 ToolTipUI // Enable round panel corners on old systems. 2024-04-06 17:07:13 +08:00
ShikiSuen 08a54c14c1 PCB // Enable round panel corners. 2024-04-06 16:38:26 +08:00
ShikiSuen 67a6bb5f87 LMMgr && Main // Allow dumping user dicts through terminal. 2024-04-02 19:12:35 +08:00
ShikiSuen 97a9e9aa5c LMAssembly // Let LMInstantiator summarize user data. 2024-04-02 19:12:35 +08:00
ShikiSuen c932083c5f LMInstantiator // Make async optional while loading user dicts. 2024-04-02 18:47:18 +08:00
ShikiSuen 25d8f7c093 LMInstantiator // Stop pinning default user weights for single reading. 2024-03-30 18:38:45 +08:00
ShikiSuen 14adf03311 LMInstantiator // Differentiate scores from factory results. 2024-03-30 18:38:45 +08:00
ShikiSuen 817df50916 LMMgr.UserPhrase // Fine-tweak suggestNextFreq(). 2024-03-30 18:31:06 +08:00
ShikiSuen e8961ff33f SessionCtl // Switch to .ofEmpty() state on toggling CapsLock. 2024-03-21 19:58:41 +08:00
ShikiSuen 2d23deb83a Tekkon // Update to v1.6.0 release. 2024-03-21 16:50:49 +08:00
ShikiSuen fc5243c97f AppDelegate // Update max RAM threshold to 1024MB. 2024-03-21 16:50:46 +08:00
ShikiSuen 9e1d130ba7 UserDef // +kMinCellWidthForHorizontalMatrix. 2024-03-10 22:00:51 +08:00
ShikiSuen 5a6aee2a25 PCB // Tweak panel opacity. 2024-03-10 22:00:51 +08:00
ShikiSuen aa4162fa9b DataCompiler // Fix SQLite random segmentation fault 11. 2024-03-10 00:03:54 +08:00
ShikiSuen 951f41461a TDKCandidates // Refactor highlightedColor(). 2024-03-09 04:14:59 +08:00
ShikiSuen f46cfda6f5 [VersionUp] 3.8.4 GM Build 3840. 2024-03-08 03:21:39 +08:00
ShikiSuen 03edccff4f Update Data - 20240308 2024-03-08 03:18:05 +08:00
ShikiSuen d8aba434d9 SecureEventInputSputnik // Patch a memory leak, etc. 2024-03-08 01:54:11 +08:00
ShikiSuen 005116c429 SPM // Consolidate dependencies. 2024-03-06 00:18:55 +08:00
ShikiSuen 93256f0095 Xcode // Add a debuggable-only target. 2024-03-04 18:34:28 +08:00
ShikiSuen 11bb5a9b66 Xcode // Stop stripping Swift symbols. 2024-03-04 17:32:25 +08:00
ShikiSuen 9dc7821708 [VersionUp] 3.8.3 GM Build 3830. 2024-03-02 23:06:53 +08:00
ShikiSuen 5d62d5b66d Update Data - 20240301 2024-03-02 23:06:53 +08:00
ShikiSuen c63c531f1b MainAssembly // Include remaining AppDelegate IBOutlets. 2024-03-02 23:06:53 +08:00
ShikiSuen 35d4426730 UserPhrase // Improve score boosting / nerfing for single kanji. 2024-03-02 23:06:53 +08:00
ShikiSuen b628ddd082 LMInstantiator // Expose factoryCoreUnigramsFor(). 2024-03-02 23:06:53 +08:00
ShikiSuen 4e00791144 LMPlainBopomofo // Fix mistakes in Eten DOS CHS Sequence Data. 2024-03-02 23:06:53 +08:00
ShikiSuen 1e098cac53 InputHandler // Prioritize the handling of the service menu. 2024-03-02 23:06:53 +08:00
ShikiSuen 0107e7cd78 ServiceMenu // Filter some services if readings are unavailable. 2024-03-02 23:06:53 +08:00
ShikiSuen 9411686d03 UserDef // +useShiftQuestionToCallServiceMenu. 2024-03-02 23:06:53 +08:00
ShikiSuen 5eec7cd604 MainAssembly // + Candidate Service (Menu & Editor). 2024-03-02 23:06:53 +08:00
ShikiSuen dc79c629a1 CandidateNode // Subclass: ServiceMenuNode. 2024-03-02 23:06:53 +08:00
ShikiSuen 040c597345 Shared // +CandidateTextService. 2024-03-02 23:06:53 +08:00
ShikiSuen 923471c8bb UserDef // +kCandidateServiceMenuContents. 2024-03-02 23:06:53 +08:00
ShikiSuen 46d4e7bdb3 MainAssembly // Refactor wherever using UniformTypeIdentifiers. 2024-03-02 23:06:53 +08:00
ShikiSuen 55dcdc8ce0 (NS)String // Add some codepoint extensions. 2024-03-02 23:06:53 +08:00
ShikiSuen bd5fdcaa26 ClientListMgr // Fix metrics and the invisible scroller. 2024-03-02 23:06:53 +08:00
ShikiSuen d5d9167b1e CocoaImpl // Fix isDarkMode(). 2024-03-02 23:06:53 +08:00
ShikiSuen c2679735c1 PrefMgr // Refactor the didSet methods. 2024-03-02 23:06:53 +08:00
ShikiSuen 4904664277 Shared // Fix a KVO Observer. 2024-03-02 23:06:53 +08:00
ShikiSuen 76dd75ce5a UserDef // +kSpecifyCmdOptCtrlEnterBehavior. 2024-03-02 23:06:53 +08:00
ShikiSuen 7be2a85b25 BrailleSputnik // Initial Implementation. 2024-03-02 23:06:53 +08:00
148 changed files with 11031 additions and 8479 deletions

View File

@ -1058,6 +1058,19 @@ func healthCheck(_ data: [Unigram]) -> String {
return result
}
// MARK: - Flags
struct TaskFlags: OptionSet {
public let rawValue: Int
public init(rawValue: Int) {
self.rawValue = rawValue
}
public static let common = TaskFlags(rawValue: 1 << 0)
public static let chs = TaskFlags(rawValue: 1 << 1)
public static let cht = TaskFlags(rawValue: 1 << 2)
}
// MARK: -
var compileJSON = false
@ -1080,61 +1093,71 @@ func main() {
NSLog("// SQLite 資料庫初期化失敗。")
exit(-1)
}
var taskFlags: TaskFlags = [.common, .chs, .cht] {
didSet {
guard taskFlags.isEmpty else { return }
NSLog("// 全部 TXT 辭典檔案建置完畢。")
if compileJSON {
NSLog("// 全部 JSON 辭典檔案建置完畢。")
}
if compileSQLite, prepared {
NSLog("// 開始整合反查資料。")
mapReverseLookupForCheck.forEach { key, values in
values.reversed().forEach { valueLiteral in
let value = cnvPhonabetToASCII(valueLiteral)
if !rangeMapReverseLookup[key, default: []].contains(value) {
rangeMapReverseLookup[key, default: []].insert(value, at: 0)
}
}
}
NSLog("// 反查資料整合完畢。")
NSLog("// 準備建置 SQL 資料庫。")
writeMainMapToSQL(rangeMapJSONCHS, column: "theDataCHS")
writeMainMapToSQL(rangeMapJSONCHT, column: "theDataCHT")
writeMainMapToSQL(rangeMapSymbols, column: "theDataSYMB")
writeMainMapToSQL(rangeMapZhuyinwen, column: "theDataCHEW")
writeMainMapToSQL(rangeMapCNS, column: "theDataCNS")
writeRevLookupMapToSQL(rangeMapReverseLookup)
let committed = "commit;".runAsSQLExec(dbPointer: &ptrSQL)
assert(committed)
let compressed = "VACUUM;".runAsSQLExec(dbPointer: &ptrSQL)
assert(compressed)
if !dumpSQLDB() {
NSLog("// SQLite 辭典傾印失敗。")
} else {
NSLog("// 全部 SQLite 辭典檔案建置完畢。")
}
sqlite3_close_v2(ptrSQL)
}
}
}
let globalQueue = DispatchQueue.global(qos: .default)
let group = DispatchGroup()
group.enter()
globalQueue.async {
NSLog("// 準備編譯符號表情ㄅ文語料檔案。")
commonFileOutput()
taskFlags.remove(.common)
group.leave()
}
group.enter()
globalQueue.async {
NSLog("// 準備編譯繁體中文核心語料檔案。")
fileOutput(isCHS: false)
taskFlags.remove(.cht)
group.leave()
}
group.enter()
globalQueue.async {
NSLog("// 準備編譯簡體中文核心語料檔案。")
fileOutput(isCHS: true)
taskFlags.remove(.chs)
group.leave()
}
//
group.wait()
NSLog("// 全部 TXT 辭典檔案建置完畢。")
if compileJSON {
NSLog("// 全部 JSON 辭典檔案建置完畢。")
}
if compileSQLite, prepared {
NSLog("// 開始整合反查資料。")
mapReverseLookupForCheck.forEach { key, values in
values.reversed().forEach { valueLiteral in
let value = cnvPhonabetToASCII(valueLiteral)
if !rangeMapReverseLookup[key, default: []].contains(value) {
rangeMapReverseLookup[key, default: []].insert(value, at: 0)
}
}
}
NSLog("// 反查資料整合完畢。")
NSLog("// 準備建置 SQL 資料庫。")
writeMainMapToSQL(rangeMapJSONCHS, column: "theDataCHS")
writeMainMapToSQL(rangeMapJSONCHT, column: "theDataCHT")
writeMainMapToSQL(rangeMapSymbols, column: "theDataSYMB")
writeMainMapToSQL(rangeMapZhuyinwen, column: "theDataCHEW")
writeMainMapToSQL(rangeMapCNS, column: "theDataCNS")
writeRevLookupMapToSQL(rangeMapReverseLookup)
let committed = "commit;".runAsSQLExec(dbPointer: &ptrSQL)
assert(committed)
let compressed = "VACUUM;".runAsSQLExec(dbPointer: &ptrSQL)
assert(compressed)
if !dumpSQLDB() {
NSLog("// SQLite 辭典傾印失敗。")
} else {
NSLog("// 全部 SQLite 辭典檔案建置完畢。")
}
sqlite3_close_v2(ptrSQL)
}
}
main()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,6 +19,7 @@ public class CandidateCellData: Hashable {
public static var unifiedSize: Double = 16
public static var unifiedCharDimension: Double { ceil(unifiedSize * 1.0125 + 7) }
public static var unifiedTextHeight: Double { ceil(unifiedSize * 19 / 16) }
static var internalPrefs = PrefMgr()
public var selectionKey: String
public let displayedText: String
public private(set) var textDimension: NSSize
@ -81,7 +82,8 @@ public class CandidateCellData: Hashable {
}
public func cellLength(isMatrix: Bool = true) -> Double {
let minLength = ceil(Self.unifiedCharDimension * 2 + size * 1.25)
let factor: CGFloat = (Self.internalPrefs.minCellWidthForHorizontalMatrix == 0) ? 1.5 : 2
let minLength = ceil(Self.unifiedCharDimension * factor + size * 1.25)
if displayedText.count <= 2, isMatrix { return minLength }
return textDimension.width
}

View File

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

View File

@ -7,7 +7,7 @@
// requirements defined in MIT License.
import AppKit
import CocoaExtension
import OSFrameworkImpl
import Shared
private extension NSUserInterfaceLayoutOrientation {

View File

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

View File

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

View File

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

View File

@ -8,8 +8,6 @@
import Foundation
import Megrez
import Shared
import SQLite3
public extension LMAssembly {
/// LMInstantiatorLMI
@ -43,17 +41,19 @@ public extension LMAssembly {
public var deltaOfCalendarYears: Int = -2000
}
public static var asyncLoadingUserData: Bool = true
// SQLite
static var ptrSQL: OpaquePointer?
// SQLite
public private(set) static var isSQLDBConnected: Bool = false
public internal(set) static var isSQLDBConnected: Bool = false
//
public let isCHS: Bool
//
public var config = Config()
public private(set) var config = Config()
// package
public init(
@ -69,21 +69,8 @@ public extension LMAssembly {
return self
}
@discardableResult public static func connectSQLDB(dbPath: String, dropPreviousConnection: Bool = true) -> Bool {
if dropPreviousConnection { disconnectSQLDB() }
vCLog("Establishing SQLite connection to: \(dbPath)")
guard sqlite3_open(dbPath, &Self.ptrSQL) == SQLITE_OK else { return false }
guard "PRAGMA journal_mode = OFF;".runAsSQLExec(dbPointer: &ptrSQL) else { return false }
isSQLDBConnected = true
return true
}
public static func disconnectSQLDB() {
if Self.ptrSQL != nil {
sqlite3_close_v2(Self.ptrSQL)
Self.ptrSQL = nil
}
isSQLDBConnected = false
public static func setCassetCandidateKeyValidator(_ validator: @escaping (String) -> Bool) {
Self.lmCassette.candidateKeysValidator = validator
}
///
@ -122,23 +109,37 @@ public extension LMAssembly {
public func resetFactoryJSONModels() {}
public func loadUserPhrasesData(path: String, filterPath: String?) {
DispatchQueue.main.async {
func loadMain() {
if FileManager.default.isReadableFile(atPath: path) {
self.lmUserPhrases.clear()
self.lmUserPhrases.open(path)
vCLog("lmUserPhrases: \(self.lmUserPhrases.count) entries of data loaded from: \(path)")
lmUserPhrases.clear()
lmUserPhrases.open(path)
vCLMLog("lmUserPhrases: \(lmUserPhrases.count) entries of data loaded from: \(path)")
} else {
vCLog("lmUserPhrases: File access failure: \(path)")
vCLMLog("lmUserPhrases: File access failure: \(path)")
}
}
if !Self.asyncLoadingUserData {
loadMain()
} else {
DispatchQueue.main.async {
loadMain()
}
}
guard let filterPath = filterPath else { return }
DispatchQueue.main.async {
func loadFilter() {
if FileManager.default.isReadableFile(atPath: filterPath) {
self.lmFiltered.clear()
self.lmFiltered.open(filterPath)
vCLog("lmFiltered: \(self.lmFiltered.count) entries of data loaded from: \(path)")
lmFiltered.clear()
lmFiltered.open(filterPath)
vCLMLog("lmFiltered: \(lmFiltered.count) entries of data loaded from: \(path)")
} else {
vCLog("lmFiltered: File access failure: \(path)")
vCLMLog("lmFiltered: File access failure: \(path)")
}
}
if !Self.asyncLoadingUserData {
loadFilter()
} else {
DispatchQueue.main.async {
loadFilter()
}
}
}
@ -148,57 +149,85 @@ public extension LMAssembly {
if FileManager.default.isReadableFile(atPath: path) {
lmFiltered.clear()
lmFiltered.open(path)
vCLog("lmFiltered: \(lmFiltered.count) entries of data loaded from: \(path)")
vCLMLog("lmFiltered: \(lmFiltered.count) entries of data loaded from: \(path)")
} else {
vCLog("lmFiltered: File access failure: \(path)")
vCLMLog("lmFiltered: File access failure: \(path)")
}
}
public func loadUserSymbolData(path: String) {
DispatchQueue.main.async {
func load() {
if FileManager.default.isReadableFile(atPath: path) {
self.lmUserSymbols.clear()
self.lmUserSymbols.open(path)
vCLog("lmUserSymbol: \(self.lmUserSymbols.count) entries of data loaded from: \(path)")
lmUserSymbols.clear()
lmUserSymbols.open(path)
vCLMLog("lmUserSymbol: \(lmUserSymbols.count) entries of data loaded from: \(path)")
} else {
vCLog("lmUserSymbol: File access failure: \(path)")
vCLMLog("lmUserSymbol: File access failure: \(path)")
}
}
if !Self.asyncLoadingUserData {
load()
} else {
DispatchQueue.main.async {
load()
}
}
}
public func loadUserAssociatesData(path: String) {
DispatchQueue.main.async {
func load() {
if FileManager.default.isReadableFile(atPath: path) {
self.lmAssociates.clear()
self.lmAssociates.open(path)
vCLog("lmAssociates: \(self.lmAssociates.count) entries of data loaded from: \(path)")
lmAssociates.clear()
lmAssociates.open(path)
vCLMLog("lmAssociates: \(lmAssociates.count) entries of data loaded from: \(path)")
} else {
vCLog("lmAssociates: File access failure: \(path)")
vCLMLog("lmAssociates: File access failure: \(path)")
}
}
if !Self.asyncLoadingUserData {
load()
} else {
DispatchQueue.main.async {
load()
}
}
}
public func loadReplacementsData(path: String) {
DispatchQueue.main.async {
func load() {
if FileManager.default.isReadableFile(atPath: path) {
self.lmReplacements.clear()
self.lmReplacements.open(path)
vCLog("lmReplacements: \(self.lmReplacements.count) entries of data loaded from: \(path)")
lmReplacements.clear()
lmReplacements.open(path)
vCLMLog("lmReplacements: \(lmReplacements.count) entries of data loaded from: \(path)")
} else {
vCLog("lmReplacements: File access failure: \(path)")
vCLMLog("lmReplacements: File access failure: \(path)")
}
}
if !Self.asyncLoadingUserData {
load()
} else {
DispatchQueue.main.async {
load()
}
}
}
public var isCassetteDataLoaded: Bool { Self.lmCassette.isLoaded }
public static func loadCassetteData(path: String) {
DispatchQueue.main.async {
func load() {
if FileManager.default.isReadableFile(atPath: path) {
Self.lmCassette.clear()
Self.lmCassette.open(path)
vCLog("lmCassette: \(Self.lmCassette.count) entries of data loaded from: \(path)")
vCLMLog("lmCassette: \(Self.lmCassette.count) entries of data loaded from: \(path)")
} else {
vCLog("lmCassette: File access failure: \(path)")
vCLMLog("lmCassette: File access failure: \(path)")
}
}
if !Self.asyncLoadingUserData {
load()
} else {
DispatchQueue.main.async {
load()
}
}
}
@ -329,11 +358,6 @@ public extension LMAssembly {
}
}
// reversed 使
//
// rawUserUnigrams
rawAllUnigrams += lmUserPhrases.unigramsFor(key: keyChain).reversed()
if !config.isCassetteEnabled || config.isCassetteEnabled && keyChain.map(\.description)[0] == "_" {
// NumPad
rawAllUnigrams += supplyNumPadUnigrams(key: keyChain)
@ -362,6 +386,21 @@ 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)

View File

@ -8,7 +8,7 @@
import Foundation
import Megrez
import Shared
import SwiftExtension
public extension LMAssembly.LMInstantiator {
///

View File

@ -8,7 +8,6 @@
import Foundation
import Megrez
import Shared
import SQLite3
/* ==============
@ -57,6 +56,23 @@ extension LMAssembly.LMInstantiator {
}
extension LMAssembly.LMInstantiator {
@discardableResult public static func connectSQLDB(dbPath: String, dropPreviousConnection: Bool = true) -> Bool {
if dropPreviousConnection { disconnectSQLDB() }
vCLMLog("Establishing SQLite connection to: \(dbPath)")
guard sqlite3_open(dbPath, &Self.ptrSQL) == SQLITE_OK else { return false }
guard "PRAGMA journal_mode = OFF;".runAsSQLExec(dbPointer: &ptrSQL) else { return false }
isSQLDBConnected = true
return true
}
public static func disconnectSQLDB() {
if Self.ptrSQL != nil {
sqlite3_close_v2(Self.ptrSQL)
Self.ptrSQL = nil
}
isSQLDBConnected = false
}
fileprivate static func querySQL(strStmt sqlQuery: String, coreColumn column: CoreColumn, handler: (String) -> Void) {
guard Self.ptrSQL != nil else { return }
performStatementSansResult { ptrStatement in
@ -125,9 +141,10 @@ extension LMAssembly.LMInstantiator {
}
/// UTF8
/// - Remark: 使
/// - parameters:
/// - key:
func factoryCoreUnigramsFor(key: String) -> [Megrez.Unigram] {
public func factoryCoreUnigramsFor(key: String) -> [Megrez.Unigram] {
// ASCII SQLite
factoryUnigramsFor(key: key, column: isCHS ? .theDataCHS : .theDataCHT)
}
@ -146,8 +163,10 @@ 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
let arrRangeRecords = currentResult.split(separator: "\t")
for strNetaSet in arrRangeRecords {
var i: Double = 0
var previousScore: Double?
currentResult.split(separator: "\t").forEach { strNetaSet in
// stable sort
let neta = Array(strNetaSet.trimmingCharacters(in: .newlines).split(separator: " ").reversed())
let theValue: String = .init(neta[0])
var theScore = column.defaultScore
@ -157,8 +176,15 @@ 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") { continue }
if !key.contains("_punctuation") { return }
let halfValue = theValue.applyingTransformFW2HW(reverse: false)
if halfValue != theValue {
gramsHW.append(Megrez.Unigram(value: halfValue, score: theScore))

View File

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

View File

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

View File

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

View File

@ -10,7 +10,6 @@
import Foundation
import LineReader
import Megrez
import Shared
extension LMAssembly {
/// 便使
@ -40,6 +39,7 @@ 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
}
@ -195,7 +195,7 @@ extension LMAssembly.LMCassette {
// Post process.
// Package 便 J / K
//
if CandidateKey.validate(keys: selectionKeys) != nil { selectionKeys = "1234567890" }
if !candidateKeysValidator(selectionKeys) { selectionKeys = "1234567890" }
if !keysUsedInCharDef.intersection(selectionKeys.map(\.description)).isEmpty {
areCandidateKeysShiftHeld = true
}
@ -204,10 +204,10 @@ extension LMAssembly.LMCassette {
filePath = path
return true
} catch {
vCLog("CIN Loading Failed: File Access Error.")
vCLMLog("CIN Loading Failed: File Access Error.")
}
} else {
vCLog("CIN Loading Failed: File Missing.")
vCLMLog("CIN Loading Failed: File Missing.")
}
filePath = oldPath
return false

View File

@ -7,8 +7,6 @@
// requirements defined in MIT License.
import Megrez
import PinyinPhonaConverter
import Shared
extension LMAssembly {
/// LMCore LMCoreEX range
@ -81,8 +79,8 @@ extension LMAssembly {
replaceData(textData: rawStrData)
} catch {
filePath = oldPath
vCLog("\(error)")
vCLog("↑ Exception happened when reading data at: \(path).")
vCLMLog("\(error)")
vCLMLog("↑ Exception happened when reading data at: \(path).")
return false
}
@ -133,7 +131,7 @@ extension LMAssembly {
}
try dataToWrite.write(toFile: filePath, atomically: true, encoding: .utf8)
} catch {
vCLog("Failed to save current database to: \(filePath)")
vCLMLog("Failed to save current database to: \(filePath)")
}
}
@ -150,7 +148,7 @@ extension LMAssembly {
strDump += addline
}
}
vCLog(strDump)
vCLMLog(strDump)
}
/// strData
@ -186,3 +184,15 @@ 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
}
}

View File

@ -7,7 +7,6 @@
// requirements defined in MIT License.
import Foundation
import Shared
extension LMAssembly {
struct LMPlainBopomofo {
@ -22,8 +21,8 @@ extension LMAssembly {
let rawJSON = try JSONDecoder().decode([String: [String: String]].self, from: rawData)
dataMap = rawJSON
} catch {
vCLog("\(error)")
vCLog("↑ Exception happened when parsing raw JSON sequence data from vChewing LMAssembly.")
vCLMLog("\(error)")
vCLMLog("↑ Exception happened when parsing raw JSON sequence data from vChewing LMAssembly.")
dataMap = [:]
}
}

View File

@ -518,7 +518,7 @@ let jsnEtenDosSequence = """
"ㄍㄡ":{"S":"句沟勾钩枸泃篝缑构芶耩","T":"句溝勾鉤枸泃篝緱构芶耩"},
"ㄍㄡˇ":{"S":"狗茍岣枸苟笱耇茩蚼","T":"狗茍岣枸苟笱耇茩蚼"},
"ㄍㄡˋ":{"S":"够购垢构媾彀构诟遘觏冓姤雊傋瞉簼鞲唦","T":"夠購垢構媾彀搆詬遘覯冓姤雊傋瞉簼韝唦"},
"ㄍㄢ":{"S":"甘干竿肝尴柑坩泔咁疳玕杆矸虷筸蜬鳱嵅","T":"乾甘干竿肝尷柑坩泔咁疳玕杆矸虷筸蜬鳱嵅"},
"ㄍㄢ":{"S":"甘干竿肝尴柑坩泔咁疳玕杆矸虷筸蜬鳱嵅","T":"乾甘干竿肝尷柑坩泔咁疳玕杆矸虷筸蜬鳱嵅"},
"ㄍㄢˇ":{"S":"敢感赶杆橄秆澉皯盰赶","T":"敢感趕桿橄稈澉皯盰赶"},
"ㄍㄢˋ":{"S":"干赣凎淦绀旰骭詌干赣涻簳嵅","T":"幹贛凎淦紺旰骭詌榦灨涻簳嵅"},
"ㄍㄣ":{"S":"跟根","T":"跟根"},
@ -843,7 +843,7 @@ let jsnEtenDosSequence = """
"ㄓㄚˊ":{"S":"扎札扎闸炸霅铡哳札蚻譗蠿","T":"扎札紮閘炸霅鍘哳劄蚻譗蠿"},
"ㄓㄚˋ":{"S":"榨栅炸诈乍榨蚱咋蜡吒溠砟醡鮓痄簎","T":"榨柵炸詐乍搾蚱咋蜡吒溠砟醡鮓痄簎"},
"ㄓㄜ":{"S":"遮螫晢嫬","T":"遮螫晢嫬"},
"ㄓㄜ˙":{"S":"遮晢嫬","T":"著遮晢嫬"},
"ㄓㄜ˙":{"S":"遮晢嫬","T":"著遮晢嫬"},
"ㄓㄜˇ":{"S":"者赭锗","T":"者赭鍺"},
"ㄓㄜˊ":{"S":"折哲摺慑褶谪辄摘辙慑蜇磔乇讋晢鮿耴悊砓謺虴鸅讘瓋","T":"折哲摺懾褶謫輒摘轍慴蜇磔乇讋晢鮿耴悊砓謺虴鸅讘瓋"},
"ㄓㄜˋ":{"S":"这浙蔗鹧柘宅檡烢蟅","T":"這浙蔗鷓柘宅檡烢蟅"},
@ -851,9 +851,9 @@ let jsnEtenDosSequence = """
"ㄓㄞˇ":{"S":"窄岝","T":"窄岝"},
"ㄓㄞˊ":{"S":"宅翟","T":"宅翟"},
"ㄓㄞˋ":{"S":"债寨祭责瘵砦","T":"債寨祭責瘵砦"},
"ㄓㄠ":{"S":"朝招昭召嘲钊駋鉊晁盄鍣妱","T":"朝招昭召著嘲釗駋鉊晁盄鍣妱"},
"ㄓㄠ":{"S":"朝招昭召嘲钊駋鉊晁盄鍣妱","T":"朝招昭召著嘲釗駋鉊晁盄鍣妱"},
"ㄓㄠˇ":{"S":"找沼爪菬瑵","T":"找沼爪菬瑵"},
"ㄓㄠˊ":{"S":"","T":""},
"ㄓㄠˊ":{"S":"","T":""},
"ㄓㄠˋ":{"S":"照赵召罩兆肇诏晁笊棹照雿狣棹箌鵫垗旐曌","T":"照趙召罩兆肇詔晁笊櫂炤雿狣棹箌鵫垗旐曌"},
"ㄓㄡ":{"S":"周周州洲舟粥賙啁盩譸輈喌騆鸼洀淍銂珘徟輖侜婤","T":"周週州洲舟粥賙啁盩譸輈喌騆鵃洀淍銂珘徟輖侜婤"},
"ㄓㄡˇ":{"S":"帚肘睭鯞","T":"帚肘睭鯞"},
@ -878,7 +878,7 @@ let jsnEtenDosSequence = """
"ㄓㄨㄚ":{"S":"抓挝髽檛","T":"抓撾髽檛"},
"ㄓㄨㄚˇ":{"S":"","T":""},
"ㄓㄨㄛ":{"S":"捉桌涿棹穛","T":"捉桌涿棹穛"},
"ㄓㄨㄛˊ":{"S":"卓茁浊拙濯酌灼啄镯擢琢诼倬斫斮梲椓焯蝃踔鷟彴汋斫禚篧浞棳謶錣啅罬斀蠗圴剢灂","T":"卓茁濁拙濯酌灼著啄鐲擢琢諑倬斲斮梲椓焯蝃踔鷟彴汋斫禚篧浞棳謶錣啅罬斀蠗圴剢灂"},
"ㄓㄨㄛˊ":{"S":"卓茁浊拙濯酌灼啄镯擢琢诼倬斫斮梲椓焯蝃踔鷟彴汋斫禚篧浞棳謶錣啅罬斀蠗圴剢灂","T":"卓茁濁拙濯酌灼著啄鐲擢琢諑倬斲斮梲椓焯蝃踔鷟彴汋斫禚篧浞棳謶錣啅罬斀蠗圴剢灂"},
"ㄓㄨㄞ":{"S":"","T":""},
"ㄓㄨㄞˇ":{"S":"","T":""},
"ㄓㄨㄞˋ":{"S":"","T":""},

View File

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

View File

@ -9,7 +9,6 @@
import Foundation
import Megrez
import Shared
// MARK: - Public Types.
@ -202,18 +201,18 @@ extension LMAssembly.LMUserOverride {
do {
let nullData = "{}"
guard let fileURL = fileURL ?? fileSaveLocationURL else {
throw "given fileURL is invalid or nil."
throw UOMError(rawValue: "given fileURL is invalid or nil.")
}
try nullData.write(to: fileURL, atomically: false, encoding: .utf8)
} catch {
vCLog("UOM Error: Unable to clear the data in the UOM file. Details: \(error)")
vCLMLog("UOM Error: Unable to clear the data in the UOM file. Details: \(error)")
return
}
}
func saveData(toURL fileURL: URL? = nil) {
guard let fileURL: URL = fileURL ?? fileSaveLocationURL else {
vCLog("UOM saveData() failed. At least the file Save URL is not set for the current UOM.")
vCLMLog("UOM saveData() failed. At least the file Save URL is not set for the current UOM.")
return
}
// 使 JSONSerialization
@ -222,14 +221,14 @@ extension LMAssembly.LMUserOverride {
guard let jsonData = try? encoder.encode(mutLRUMap) else { return }
try jsonData.write(to: fileURL, options: .atomic)
} catch {
vCLog("UOM Error: Unable to save data, abort saving. Details: \(error)")
vCLMLog("UOM Error: Unable to save data, abort saving. Details: \(error)")
return
}
}
func loadData(fromURL fileURL: URL? = nil) {
guard let fileURL: URL = fileURL ?? fileSaveLocationURL else {
vCLog("UOM loadData() failed. At least the file Load URL is not set for the current UOM.")
vCLMLog("UOM loadData() failed. At least the file Load URL is not set for the current UOM.")
return
}
// 使 JSONSerialization
@ -238,13 +237,13 @@ extension LMAssembly.LMUserOverride {
let data = try Data(contentsOf: fileURL, options: .mappedIfSafe)
if ["", "{}"].contains(String(data: data, encoding: .utf8)) { return }
guard let jsonResult = try? decoder.decode([String: KeyObservationPair].self, from: data) else {
vCLog("UOM Error: Read file content type invalid, abort loading.")
vCLMLog("UOM Error: Read file content type invalid, abort loading.")
return
}
mutLRUMap = jsonResult
resetMRUList()
} catch {
vCLog("UOM Error: Unable to read file or parse the data, abort loading. Details: \(error)")
vCLMLog("UOM Error: Unable to read file or parse the data, abort loading. Details: \(error)")
return
}
}
@ -271,7 +270,7 @@ extension LMAssembly.LMUserOverride {
mutLRUMap.removeValue(forKey: mutLRUList[mutLRUList.endIndex - 1].key)
mutLRUList.removeLast()
}
vCLog("UOM: Observation finished with new observation: \(key)")
vCLMLog("UOM: Observation finished with new observation: \(key)")
saveCallback?() ?? saveData()
return
}
@ -282,7 +281,7 @@ extension LMAssembly.LMUserOverride {
)
mutLRUList.insert(theNeta, at: 0)
mutLRUMap[key] = theNeta
vCLog("UOM: Observation finished with existing observation: \(key)")
vCLMLog("UOM: Observation finished with existing observation: \(key)")
saveCallback?() ?? saveData()
}
}
@ -400,3 +399,10 @@ extension LMAssembly.LMUserOverride {
return result
}
}
struct UOMError: LocalizedError {
var rawValue: String
var errorDescription: String? {
NSLocalizedString("rawValue", comment: "")
}
}

View File

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

View File

@ -17,8 +17,9 @@ let package = Package(
.package(path: "../HangarRash_SwiftyCapsLockToggler"),
.package(path: "../Jad_BookmarkManager"),
.package(path: "../Qwertyyb_ShiftKeyUpChecker"),
.package(path: "../vChewing_BrailleSputnik"),
.package(path: "../vChewing_CandidateWindow"),
.package(path: "../vChewing_CocoaExtension"),
.package(path: "../vChewing_OSFrameworkImpl"),
.package(path: "../vChewing_Hotenka"),
.package(path: "../vChewing_IMKUtils"),
.package(path: "../vChewing_KimoDataReader"),
@ -38,9 +39,10 @@ let package = Package(
.target(
name: "MainAssembly",
dependencies: [
.product(name: "BrailleSputnik", package: "vChewing_BrailleSputnik"),
.product(name: "BookmarkManager", package: "Jad_BookmarkManager"),
.product(name: "CandidateWindow", package: "vChewing_CandidateWindow"),
.product(name: "CocoaExtension", package: "vChewing_CocoaExtension"),
.product(name: "OSFrameworkImpl", package: "vChewing_OSFrameworkImpl"),
.product(name: "FolderMonitor", package: "DanielGalasko_FolderMonitor"),
.product(name: "Hotenka", package: "vChewing_Hotenka"),
.product(name: "IMKUtils", package: "vChewing_IMKUtils"),

View File

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

View File

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

View File

@ -69,6 +69,8 @@ public extension AppDelegate {
SpeechSputnik.shared.refreshStatus() //
CandidateTextService.enableFinalSanityCheck()
// 使
// Debug
// Debug
@ -145,7 +147,7 @@ public extension AppDelegate {
guard let currentMemorySizeInBytes = NSApplication.memoryFootprint else { return 0 }
let currentMemorySize: Double = (Double(currentMemorySizeInBytes) / 1024 / 1024).rounded(toPlaces: 1)
switch currentMemorySize {
case 384...:
case 1024...:
vCLog("WARNING: EXCESSIVE MEMORY FOOTPRINT (\(currentMemorySize)MB).")
let msgPackage = UNMutableNotificationContent()
msgPackage.title = NSLocalizedString("vChewing", comment: "")
@ -167,4 +169,10 @@ public extension AppDelegate {
}
return currentMemorySize
}
// New About Window
@IBAction func about(_: Any) {
CtlAboutUI.show()
NSApp.popup()
}
}

View File

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

View File

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

View File

@ -9,12 +9,12 @@
import AppKit
import Foundation
import Shared
import UniformTypeIdentifiers
public class VwrClientListMgr: NSViewController {
let windowWidth: CGFloat = 770
let contentWidth: CGFloat = 750
let buttonWidth: CGFloat = 150
let tableHeight: CGFloat = 230
lazy var tblClients: NSTableView = .init()
lazy var btnAddClient = NSButton("Add Client", target: self, action: #selector(btnAddClientClicked(_:)))
@ -35,7 +35,7 @@ public class VwrClientListMgr: NSViewController {
var body: NSView? {
NSStackView.build(.vertical, insets: .new(all: 14)) {
makeScrollableTable()
.makeSimpleConstraint(.height, relation: .equal, value: 232)
.makeSimpleConstraint(.height, relation: .equal, value: tableHeight)
NSStackView.build(.horizontal) {
let descriptionWidth = contentWidth - buttonWidth - 20
NSStackView.build(.vertical) {
@ -59,6 +59,8 @@ public class VwrClientListMgr: NSViewController {
scrollContainer.scrollerStyle = .legacy
scrollContainer.autohidesScrollers = true
scrollContainer.documentView = tblClients
scrollContainer.hasVerticalScroller = true
scrollContainer.hasHorizontalScroller = true
if #available(macOS 11.0, *) {
tblClients.style = .inset
}
@ -72,12 +74,12 @@ public class VwrClientListMgr: NSViewController {
tblClients.autosaveTableColumns = false
tblClients.backgroundColor = NSColor.controlBackgroundColor
tblClients.columnAutoresizingStyle = .lastColumnOnlyAutoresizingStyle
tblClients.frame = CGRect(x: 0, y: 0, width: 728, height: 230)
tblClients.frame = CGRect(x: 0, y: 0, width: 728, height: tableHeight)
tblClients.gridColor = NSColor.clear
tblClients.intercellSpacing = CGSize(width: 17, height: 0)
tblClients.rowHeight = 24
tblClients.setContentHuggingPriority(.defaultHigh, for: .vertical)
tblClients.registerForDraggedTypes([.init(rawValue: kUTTypeFileURL as String)])
tblClients.registerForDraggedTypes([.kUTTypeFileURL])
tblClients.dataSource = self
tblClients.action = #selector(onItemClicked(_:))
tblClients.target = self
@ -150,7 +152,7 @@ extension VwrClientListMgr {
neta info: NSDraggingInfo, onError: @escaping () -> Void?, handler: (([URL]) -> Void)? = nil
) {
let board = info.draggingPasteboard
let type = NSPasteboard.PasteboardType(rawValue: kUTTypeApplicationBundle as String)
let type = NSPasteboard.PasteboardType.kUTTypeAppBundle
let options: [NSPasteboard.ReadingOptionKey: Any] = [
.urlReadingFileURLsOnly: true,
.urlReadingContentsConformToTypes: [type],

View File

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

View File

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

View File

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

View File

@ -58,7 +58,7 @@ private extension InputHandler {
func narrateTheComposer(with maybeKey: String? = nil, when condition: Bool, allowDuplicates: Bool = true) {
guard condition else { return }
let maybeKey = maybeKey ?? composer.phonabetKeyForQuery(pronouncable: prefs.acceptLeadingIntonations)
let maybeKey = maybeKey ?? composer.phonabetKeyForQuery(pronounceableOnly: prefs.acceptLeadingIntonations)
guard var keyToNarrate = maybeKey else { return }
if composer.intonation == Tekkon.Phonabet(" ") { keyToNarrate.append("ˉ") }
SpeechSputnik.shared.narrate(keyToNarrate, allowDuplicates: allowDuplicates)
@ -119,7 +119,7 @@ private extension InputHandler {
return handleEnter(input: input, readingOnly: true)
}
//
let maybeKey = composer.phonabetKeyForQuery(pronouncable: prefs.acceptLeadingIntonations)
let maybeKey = composer.phonabetKeyForQuery(pronounceableOnly: prefs.acceptLeadingIntonations)
guard let readingKey = maybeKey else { break ifComposeReading }
//
if !currentLM.hasUnigramsFor(keyArray: [readingKey]) {
@ -204,7 +204,7 @@ private extension InputHandler {
/// 調
if keyConsumedByReading {
// strict false
if composer.phonabetKeyForQuery(pronouncable: false) == nil {
if composer.phonabetKeyForQuery(pronounceableOnly: false) == nil {
// 調
if !composer.isPinyinMode, input.isSpace,
compositor.insertKey(existedIntonation.value)

View File

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

View File

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

View File

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

View File

@ -7,6 +7,7 @@
// requirements defined in MIT License.
import Foundation
import Shared
import SwiftExtension
// MARK: - Typing Method

View File

@ -65,6 +65,11 @@ 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())
}

View File

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

View File

@ -345,3 +345,26 @@ public extension LMMgr {
}
}
}
// MARK: - 使 JSON
// JSON 便
public extension LMMgr {
@discardableResult static func dumpUserDictDataToJSON(print: Bool = false, all: Bool) -> String? {
var summarizedDict = [LMAssembly.UserDictionarySummarized]()
Shared.InputMode.allCases.forEach { mode in
guard mode != .imeModeNULL else { return }
summarizedDict.append(mode.langModel.summarize(all: all))
}
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
if #available(macOS 10.13, *) {
encoder.outputFormatting.insert(.sortedKeys)
}
guard let data = try? encoder.encode(summarizedDict) else { return nil }
guard let outputStr = String(data: data, encoding: .utf8) else { return nil }
if print { Swift.print(outputStr) }
return outputStr
}
}

View File

@ -0,0 +1,36 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import Shared
public extension PrefMgr {
static let shared: PrefMgr = {
let result = PrefMgr()
result.assignDidSetActions()
return result
}()
private func assignDidSetActions() {
didAskForSyncingLMPrefs = {
if PrefMgr.shared.phraseReplacementEnabled {
LMMgr.loadUserPhraseReplacement()
}
if PrefMgr.shared.associatedPhrasesEnabled {
LMMgr.loadUserAssociatesData()
}
LMMgr.syncLMPrefs()
}
didAskForRefreshingSpeechSputnik = {
SpeechSputnik.shared.refreshStatus()
}
didAskForSyncingShiftKeyDetectorPrefs = {
SessionCtl.theShiftKeyDetector.toggleWithLShift = PrefMgr.shared.togglingAlphanumericalModeWithLShift
SessionCtl.theShiftKeyDetector.toggleWithRShift = PrefMgr.shared.togglingAlphanumericalModeWithRShift
}
}
}

View File

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

View File

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

View File

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

View File

@ -7,10 +7,10 @@
// requirements defined in MIT License.
import CandidateWindow
import CocoaExtension
import IMKUtils
import InputMethodKit
import NotifierUI
import OSFrameworkImpl
import PopupCompositionBuffer
import Shared
import ShiftKeyUpChecker

View File

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

View File

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

View File

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

View File

@ -7,8 +7,8 @@
// requirements defined in MIT License.
import AppKit
import CocoaExtension
import NotifierUI
import OSFrameworkImpl
import Shared
import SwiftExtension
@ -129,6 +129,9 @@ extension SessionCtl {
NSMenu.Item(verbatim: "Client Manager".localized.withEllipsis)?
.act(#selector(showClientListMgr(_:)))
.nulled(silentMode)
NSMenu.Item(verbatim: "Service Menu Editor".localized.withEllipsis)?
.act(#selector(showServiceMenuEditor(_:)))
.alternated().nulled(silentMode)
NSMenu.Item("Check for Updates…")?
.act(#selector(checkForUpdate(_:)))
.nulled(silentMode)
@ -184,6 +187,11 @@ public extension SessionCtl {
NSApp.popup()
}
@objc func showServiceMenuEditor(_: Any? = nil) {
CtlServiceMenuEditor.show()
NSApp.popup()
}
@objc func toggleCassetteMode(_: Any? = nil) {
resetInputHandler(forceComposerCleanup: true)
if !PrefMgr.shared.cassetteEnabled, !LMMgr.checkCassettePathValidity(PrefMgr.shared.cassettePath) {

View File

@ -33,7 +33,11 @@ public extension SettingsPanesCocoa {
UserDef.kSpecifyShiftBackSpaceKeyBehavior.render(fixWidth: innerContentWidth)
UserDef.kSpecifyShiftTabKeyBehavior.render(fixWidth: innerContentWidth)
UserDef.kSpecifyShiftSpaceKeyBehavior.render(fixWidth: innerContentWidth)
UserDef.kSpecifyCmdOptCtrlEnterBehavior.render(fixWidth: innerContentWidth)
}?.boxed()
NSView()
}
NSTabView.TabPage(title: "") {
NSStackView.buildSection(width: innerContentWidth) {
UserDef.kUpperCaseLetterKeyBehavior.render(fixWidth: innerContentWidth)
UserDef.kNumPadCharInputBehavior.render(fixWidth: innerContentWidth)
@ -44,7 +48,7 @@ public extension SettingsPanesCocoa {
}?.boxed()
NSView()
}
NSTabView.TabPage(title: "") {
NSTabView.TabPage(title: "") {
NSStackView.buildSection(width: innerContentWidth) {
UserDef.kChooseCandidateUsingSpace.render(fixWidth: innerContentWidth)
UserDef.kEscToCleanInputBuffer.render(fixWidth: innerContentWidth)
@ -65,7 +69,7 @@ public extension SettingsPanesCocoa {
}?.boxed()
NSView()
}
NSTabView.TabPage(title: "") {
NSTabView.TabPage(title: "") {
NSStackView.buildSection(width: innerContentWidth) {
UserDef.kBypassNonAppleCapsLockHandling.render(fixWidth: innerContentWidth)
UserDef.kShareAlphanumericalModeStatusAcrossClients.render(fixWidth: innerContentWidth)

View File

@ -37,6 +37,7 @@ public extension SettingsPanesCocoa {
}
UserDef.kCandidateWindowShowOnlyOneLine.render(fixWidth: innerContentWidth)
UserDef.kAlwaysExpandCandidateWindow.render(fixWidth: innerContentWidth)
UserDef.kMinCellWidthForHorizontalMatrix.render(fixWidth: innerContentWidth)
UserDef.kRespectClientAccentColor.render(fixWidth: innerContentWidth)
}?.boxed()
NSView()
@ -54,10 +55,15 @@ public extension SettingsPanesCocoa {
UserDef.kMoveCursorAfterSelectingCandidate.render(fixWidth: innerContentWidth)
UserDef.kUseDynamicCandidateWindowOrigin.render(fixWidth: innerContentWidth)
UserDef.kDodgeInvalidEdgeCandidateCursorPosition.render(fixWidth: innerContentWidth)
UserDef.kUseShiftQuestionToCallServiceMenu
.render(fixWidth: innerContentWidth) { renderable in
renderable.currentControl?.target = self
renderable.currentControl?.action = #selector(self.performCandidateKeysSanityCheck(_:))
}
UserDef.kUseJKtoMoveCompositorCursorInCandidateState
.render(fixWidth: innerContentWidth) { renderable in
renderable.currentControl?.target = self
renderable.currentControl?.action = #selector(self.useJKToMoveBufferCursorDidSet(_:))
renderable.currentControl?.action = #selector(self.performCandidateKeysSanityCheck(_:))
}
}?.boxed()
NSView()
@ -91,7 +97,7 @@ public extension SettingsPanesCocoa {
window.callAlert(title: title.localized, text: explanation.localized)
}
@IBAction func useJKToMoveBufferCursorDidSet(_: NSControl) {
@IBAction func performCandidateKeysSanityCheck(_: NSControl) {
// didSet
PrefMgr.shared.candidateKeys = PrefMgr.shared.candidateKeys
}

View File

@ -8,8 +8,8 @@
import AppKit
import BookmarkManager
import CocoaExtension
import Foundation
import OSFrameworkImpl
import Shared
public extension SettingsPanesCocoa {

View File

@ -35,6 +35,9 @@ public struct VwrSettingsPaneBehavior: View {
@AppStorage(wrappedValue: false, UserDef.kSpecifyShiftSpaceKeyBehavior.rawValue)
private var specifyShiftSpaceKeyBehavior: Bool
@AppStorage(wrappedValue: 0, UserDef.kSpecifyCmdOptCtrlEnterBehavior.rawValue)
private var specifyCmdOptCtrlEnterBehavior: Int
@AppStorage(wrappedValue: true, UserDef.kUseSpaceToCommitHighlightedSCPCCandidate.rawValue)
private var useSpaceToCommitHighlightedSCPCCandidate: Bool
@ -80,6 +83,7 @@ public struct VwrSettingsPaneBehavior: View {
UserDef.kSpecifyShiftBackSpaceKeyBehavior.bind($specifyShiftBackSpaceKeyBehavior).render()
UserDef.kSpecifyShiftTabKeyBehavior.bind($specifyShiftTabKeyBehavior).render()
.pickerStyle(RadioGroupPickerStyle())
UserDef.kSpecifyCmdOptCtrlEnterBehavior.bind($specifyCmdOptCtrlEnterBehavior).render()
VStack(alignment: .leading) {
UserDef.kSpecifyShiftSpaceKeyBehavior.bind($specifyShiftSpaceKeyBehavior).render()
UserDef.kUseSpaceToCommitHighlightedSCPCCandidate.bind($useSpaceToCommitHighlightedSCPCCandidate).render()

View File

@ -26,6 +26,9 @@ public struct VwrSettingsPaneCandidates: View {
@AppStorage(wrappedValue: true, UserDef.kRespectClientAccentColor.rawValue)
private var respectClientAccentColor: Bool
@AppStorage(wrappedValue: 0, UserDef.kMinCellWidthForHorizontalMatrix.rawValue)
private var minCellWidthForHorizontalMatrix: Int
@AppStorage(wrappedValue: false, UserDef.kAlwaysExpandCandidateWindow.rawValue)
private var alwaysExpandCandidateWindow: Bool
@ -41,6 +44,9 @@ public struct VwrSettingsPaneCandidates: View {
@AppStorage(wrappedValue: false, UserDef.kUseJKtoMoveCompositorCursorInCandidateState.rawValue)
private var useJKtoMoveCompositorCursorInCandidateState: Bool
@AppStorage(wrappedValue: true, UserDef.kUseShiftQuestionToCallServiceMenu.rawValue)
public var useShiftQuestionToCallServiceMenu: Bool
@AppStorage(wrappedValue: true, UserDef.kMoveCursorAfterSelectingCandidate.rawValue)
private var moveCursorAfterSelectingCandidate: Bool
@ -72,6 +78,12 @@ public struct VwrSettingsPaneCandidates: View {
.disabled(useRearCursorMode)
}
UserDef.kDodgeInvalidEdgeCandidateCursorPosition.bind($dodgeInvalidEdgeCandidateCursorPosition).render()
UserDef.kUseShiftQuestionToCallServiceMenu.bind(
$useShiftQuestionToCallServiceMenu.didChange {
// didSet
PrefMgr.shared.candidateKeys = PrefMgr.shared.candidateKeys
}
).render()
UserDef.kUseJKtoMoveCompositorCursorInCandidateState.bind(
$useJKtoMoveCompositorCursorInCandidateState.didChange {
// didSet
@ -93,6 +105,8 @@ public struct VwrSettingsPaneCandidates: View {
if !candidateWindowShowOnlyOneLine {
UserDef.kAlwaysExpandCandidateWindow.bind($alwaysExpandCandidateWindow).render()
.disabled(candidateWindowShowOnlyOneLine)
UserDef.kMinCellWidthForHorizontalMatrix.bind($minCellWidthForHorizontalMatrix).render()
.disabled(candidateWindowShowOnlyOneLine)
}
UserDef.kRespectClientAccentColor.bind($respectClientAccentColor).render()
}

View File

@ -7,8 +7,8 @@
// requirements defined in MIT License.
import BookmarkManager
import OSFrameworkImpl
import Shared
import SwiftExtension
import SwiftUI
@available(macOS 13, *)

View File

@ -7,11 +7,10 @@
// requirements defined in MIT License.
import BookmarkManager
import CocoaExtension
import OSFrameworkImpl
import Shared
import SwiftExtension
import SwiftUI
import UniformTypeIdentifiers
@available(macOS 13, *)
public struct VwrSettingsPaneDictionary: View {

View File

@ -8,6 +8,7 @@
import AppKit
import AVFoundation
import Shared
public class SpeechSputnik {
public static var shared: SpeechSputnik = .init()

View File

@ -0,0 +1,141 @@
// (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 InputMethodKit
import LangModelAssembly
@testable import MainAssembly
import OSFrameworkImpl
import Shared
import XCTest
class CandidateServiceCoordinatorTests: XCTestCase {
static let testDataMap: [String] = [
#"Unicode Metadata: %s"# + "\t" + #"@SEL:copyUnicodeMetadata:"#,
#"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:"#,
]
func testSelector_FinalSanityCheck() throws {
var stacked = Self.testDataMap.parseIntoCandidateTextServiceStack(
candidate: "胡桃", reading: [] // 使 Reading
)
let count1 = stacked.count
print("Current Count before Sanity Check ON: \(stacked.count)")
CandidateTextService.enableFinalSanityCheck()
stacked = Self.testDataMap.parseIntoCandidateTextServiceStack(
candidate: "胡桃", reading: [] // 使 Reading
)
let count2 = stacked.count
print("Current Count after Sanity Check ON: \(stacked.count)")
XCTAssertGreaterThan(count1, count2)
}
func testSelector_UnicodeMetadata() throws {
let stacked = Self.testDataMap.parseIntoCandidateTextServiceStack(
candidate: "胡桃", reading: ["ㄏㄨˊ", "ㄊㄠˊ"]
)
let theService = stacked[0]
switch theService.value {
case .url: break
case .selector:
let response = theService.responseFromSelector
let expectedResponse = "胡 U+80E1 CJK UNIFIED IDEOGRAPH-80E1\n桃 U+6843 CJK UNIFIED IDEOGRAPH-6843"
XCTAssertEqual(response, expectedResponse)
}
}
func testSelector_HTMLRubyZhuyinTextbookStyle() throws {
let stacked = Self.testDataMap.parseIntoCandidateTextServiceStack(
candidate: "甜的", reading: ["ㄊㄧㄢˊ", "ㄉㄜ˙"]
)
let theService = stacked[1]
switch theService.value {
case .url: break
case .selector:
let response = theService.responseFromSelector
let expectedResponse = "<ruby>甜<rp>(</rp><rt>ㄊㄧㄢˊ</rt><rp>)</rp></ruby><ruby>的<rp>(</rp><rt>˙ㄉㄜ</rt><rp>)</rp></ruby>"
XCTAssertEqual(response, expectedResponse)
}
}
func testSelector_HTMLRubyPinyinTextbookStyle() throws {
let stacked = Self.testDataMap.parseIntoCandidateTextServiceStack(
candidate: "鐵嘴", reading: ["ㄊㄧㄝˇ", "ㄗㄨㄟˇ"]
)
let theService = stacked[2]
switch theService.value {
case .url: break
case .selector:
let response = theService.responseFromSelector
let expectedResponse = "<ruby>鐵<rp>(</rp><rt>tiě</rt><rp>)</rp></ruby><ruby>嘴<rp>(</rp><rt>zuǐ</rt><rp>)</rp></ruby>"
XCTAssertEqual(response, expectedResponse)
}
}
func testSelector_InlineAnnotationZhuyinTextbookStyle() throws {
let stacked = Self.testDataMap.parseIntoCandidateTextServiceStack(
candidate: "甜的", reading: ["ㄊㄧㄢˊ", "ㄉㄜ˙"]
)
let theService = stacked[3]
switch theService.value {
case .url: break
case .selector:
let response = theService.responseFromSelector
let expectedResponse = "甜(ㄊㄧㄢˊ)的(˙ㄉㄜ)"
XCTAssertEqual(response, expectedResponse)
}
}
func testSelector_InlineAnnotationTextbookStyle() throws {
let stacked = Self.testDataMap.parseIntoCandidateTextServiceStack(
candidate: "鐵嘴", reading: ["ㄊㄧㄝˇ", "ㄗㄨㄟˇ"]
)
let theService = stacked[4]
switch theService.value {
case .url: break
case .selector:
let response = theService.responseFromSelector
let expectedResponse = "鐵(tiě)嘴(zuǐ)"
XCTAssertEqual(response, expectedResponse)
}
}
func testSelector_Braille1947() throws {
let stacked = Self.testDataMap.parseIntoCandidateTextServiceStack(
candidate: "高科技公司的",
reading: ["ㄍㄠ", "ㄎㄜ", "ㄐㄧˋ", "ㄍㄨㄥ", "", "ㄉㄜ˙"]
)
let theService = stacked[5]
switch theService.value {
case .url: break
case .selector:
let response = theService.responseFromSelector
let expectedResponse = "⠅⠩⠄⠇⠮⠄⠅⠡⠐⠅⠯⠄⠑⠄⠙⠮⠁"
XCTAssertEqual(response, expectedResponse)
}
}
func testSelector_Braille2018() throws {
let stacked = Self.testDataMap.parseIntoCandidateTextServiceStack(
candidate: "高科技公司的",
reading: ["ㄍㄠ", "ㄎㄜ", "ㄐㄧˋ", "ㄍㄨㄥ", "", "ㄉㄜ˙"]
)
let theService = stacked[6]
switch theService.value {
case .url: break
case .selector:
let response = theService.responseFromSelector
let expectedResponse = "⠛⠖⠁⠅⠢⠁⠛⠊⠆⠛⠲⠁⠎⠁⠙⠢"
XCTAssertEqual(response, expectedResponse)
}
}
}

View File

@ -6,10 +6,10 @@
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import CocoaExtension
import InputMethodKit
import LangModelAssembly
@testable import MainAssembly
import OSFrameworkImpl
import Shared
import XCTest

View File

@ -6,10 +6,10 @@
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import CocoaExtension
import InputMethodKit
import LangModelAssembly
@testable import MainAssembly
import OSFrameworkImpl
import Shared
import XCTest
@ -151,4 +151,35 @@ extension MainAssemblyTests {
XCTAssertEqual(resultText6, "濟公的年中獎金")
vCTestLog("- 已成功證實「年終」的記憶不會對除了給定上下文以外的情形生效。")
}
/// inputHandler.commissionByCtrlOptionCommandEnter()
func test105_InputHandler_MiscCommissionTest() throws {
PrefMgr.shared.useSCPCTypingMode = false
clearTestUOM()
vCTestLog("正在測試 inputHandler.commissionByCtrlOptionCommandEnter()。")
testSession.resetInputHandler(forceComposerCleanup: true)
typeSentenceOrCandidates("el dk ru4ej/ n 2k7")
guard let handler = testSession.inputHandler as? InputHandler else {
XCTAssertThrowsError("testSession.handler is nil.")
return
}
PrefMgr.shared.specifyCmdOptCtrlEnterBehavior = 0
var result = handler.commissionByCtrlOptionCommandEnter(isShiftPressed: true)
XCTAssertEqual(result, "ㄍㄠ ㄎㄜ ㄐㄧˋ ㄍㄨㄥ ㄙ ˙ㄉㄜ")
result = handler.commissionByCtrlOptionCommandEnter() // isShiftPressed false
XCTAssertEqual(result, "高(ㄍㄠ)科(ㄎㄜ)技(ㄐㄧˋ)公(ㄍㄨㄥ)司(ㄙ)的(˙ㄉㄜ)")
PrefMgr.shared.specifyCmdOptCtrlEnterBehavior = 1
result = handler.commissionByCtrlOptionCommandEnter()
let expectedRubyResult = """
<ruby><rp>(</rp><rt></rt><rp>)</rp></ruby><ruby><rp>(</rp><rt></rt><rp>)</rp></ruby><ruby><rp>(</rp><rt>ˋ</rt><rp>)</rp></ruby><ruby><rp>(</rp><rt></rt><rp>)</rp></ruby><ruby><rp>(</rp><rt></rt><rp>)</rp></ruby><ruby><rp>(</rp><rt>˙</rt><rp>)</rp></ruby>
"""
XCTAssertEqual(result, expectedRubyResult)
PrefMgr.shared.specifyCmdOptCtrlEnterBehavior = 2
result = handler.commissionByCtrlOptionCommandEnter()
XCTAssertEqual(result, "⠅⠩⠄⠇⠮⠄⠅⠡⠐⠅⠯⠄⠑⠄⠙⠮⠁")
PrefMgr.shared.specifyCmdOptCtrlEnterBehavior = 3
result = handler.commissionByCtrlOptionCommandEnter()
XCTAssertEqual(result, "⠛⠖⠁⠅⠢⠁⠛⠊⠆⠛⠲⠁⠎⠁⠙⠢")
vCTestLog("成功完成測試 inputHandler.commissionByCtrlOptionCommandEnter()。")
}
}

View File

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

View File

@ -7,7 +7,7 @@
// requirements defined in MIT License.
import AppKit
import CocoaExtension
import OSFrameworkImpl
public class Notifier: NSWindowController {
public static func notify(message: String) {

View File

@ -2,14 +2,14 @@
import PackageDescription
let package = Package(
name: "CocoaExtension",
name: "OSFrameworkImpl",
platforms: [
.macOS(.v11),
],
products: [
.library(
name: "CocoaExtension",
targets: ["CocoaExtension"]
name: "OSFrameworkImpl",
targets: ["OSFrameworkImpl"]
),
],
dependencies: [
@ -17,7 +17,7 @@ let package = Package(
],
targets: [
.target(
name: "CocoaExtension",
name: "OSFrameworkImpl",
dependencies: [
.product(name: "SwiftExtension", package: "vChewing_SwiftExtension"),
]

View File

@ -1,4 +1,4 @@
# CocoaExtension
# OSFrameworkImpl
威注音輸入法針對 Cocoa 的一些功能擴充,使程式維護體驗更佳。

View File

@ -71,6 +71,18 @@ 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
@ -128,15 +140,14 @@ public extension NSApplication {
// MARK: - System Dark Mode Status Detector.
static var isDarkMode: Bool {
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")
// "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
}
return false
}
// MARK: - Tell whether this IME is running with Root privileges.
@ -199,19 +210,6 @@ 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 {
@ -327,3 +325,13 @@ 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")
}

View File

@ -460,12 +460,33 @@ public extension NSMenuItem {
var allowedTypes: [String] = ["txt"]
public init() {
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 = "DRAG FILE TO HERE"
registerForDraggedTypes([.init(rawValue: kUTTypeFileURL as String)])
target = self
title = givenTitle ?? "DRAG FILE TO HERE"
registerForDraggedTypes([.kUTTypeFileURL])
self.target = target ?? self
self.action = action
postDragHandler = postDrag ?? postDragHandler
}
required init?(coder: NSCoder) {
@ -478,7 +499,7 @@ public extension NSMenuItem {
fileprivate func checkExtension(_ drag: NSDraggingInfo) -> Bool {
guard let pasteboard = drag.draggingPasteboard.propertyList(
forType: NSPasteboard.PasteboardType(rawValue: "NSFilenamesPboardType")
forType: NSPasteboard.PasteboardType.kNSFilenamesPboardType
) as? [String], let path = pasteboard.first else {
return false
}
@ -494,7 +515,7 @@ public extension NSMenuItem {
override public func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
guard let pasteboard = sender.draggingPasteboard.propertyList(
forType: NSPasteboard.PasteboardType(rawValue: "NSFilenamesPboardType")
forType: NSPasteboard.PasteboardType.kNSFilenamesPboardType
) as? [String], let path = pasteboard.first else {
print("failure")
return false

View File

@ -20,15 +20,19 @@ 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)
}
@ -75,7 +79,7 @@ public extension NSWorkspace {
public static let hibernating = ActivationFlags(rawValue: 1 << 0)
public static let desktopLocked = ActivationFlags(rawValue: 1 << 1)
public static let sesssionSwitchedOut = ActivationFlags(rawValue: 1 << 2)
public static let sessionSwitchedOut = ActivationFlags(rawValue: 1 << 2)
public static let screenSaverRunning = ActivationFlags(rawValue: 1 << 3)
}
@ -123,11 +127,11 @@ extension SecureEventInputSputnik {
.store(in: &Self.combinePool)
NSWorkspace.shared.notificationCenter
.publisher(for: NSWorkspace.sessionDidResignActiveNotification)
.sink { _ in NSWorkspace.activationFlags.insert(.sesssionSwitchedOut) }
.sink { _ in NSWorkspace.activationFlags.insert(.sessionSwitchedOut) }
.store(in: &Self.combinePool)
NSWorkspace.shared.notificationCenter
.publisher(for: NSWorkspace.sessionDidBecomeActiveNotification)
.sink { _ in NSWorkspace.activationFlags.remove(.sesssionSwitchedOut) }
.sink { _ in NSWorkspace.activationFlags.remove(.sessionSwitchedOut) }
.store(in: &Self.combinePool)
} else {
Self.combinePoolCocoa.append(
@ -169,13 +173,13 @@ extension SecureEventInputSputnik {
Self.combinePoolCocoa.append(
NSWorkspace.shared.notificationCenter
.addObserver(forName: NSWorkspace.sessionDidResignActiveNotification, object: nil, queue: .main) { _ in
NSWorkspace.activationFlags.insert(.sesssionSwitchedOut)
NSWorkspace.activationFlags.insert(.sessionSwitchedOut)
}
)
Self.combinePoolCocoa.append(
NSWorkspace.shared.notificationCenter
.addObserver(forName: NSWorkspace.sessionDidBecomeActiveNotification, object: nil, queue: .main) { _ in
NSWorkspace.activationFlags.remove(.sesssionSwitchedOut)
NSWorkspace.activationFlags.remove(.sessionSwitchedOut)
}
)
}

View File

@ -6,6 +6,7 @@
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import SwiftExtension
import SwiftUI
// MARK: - Add "didChange" support to bindings.

View File

@ -10,6 +10,7 @@ import AppKit
import Combine
import Foundation
import LangModelAssembly
import OSFrameworkImpl
import Shared
import SwiftExtension
import SwiftUI

View File

@ -1,22 +0,0 @@
// swift-tools-version:5.3
import PackageDescription
let package = Package(
name: "PinyinPhonaConverter",
platforms: [
.macOS(.v11),
],
products: [
.library(
name: "PinyinPhonaConverter",
targets: ["PinyinPhonaConverter"]
),
],
dependencies: [],
targets: [
.target(
name: "PinyinPhonaConverter",
dependencies: []
),
]
)

View File

@ -1,92 +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 AppKit
public extension String {
mutating func convertToPhonabets(newToneOne: String = "") {
if isEmpty || contains("_") || !isNotPureAlphanumerical { return }
for key in arrHanyuPinyinToPhonabets {
self = replacingOccurrences(of: key.0, with: key.1)
}
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 arrHanyuPinyinToPhonabets: [(String, String)] = [
("chuang", "ㄔㄨㄤ"), ("shuang", "ㄕㄨㄤ"), ("zhuang", "ㄓㄨㄤ"), ("chang", "ㄔㄤ"), ("cheng", "ㄔㄥ"), ("chong", "ㄔㄨㄥ"),
("chuai", "ㄔㄨㄞ"), ("chuan", "ㄔㄨㄢ"), ("guang", "ㄍㄨㄤ"), ("huang", "ㄏㄨㄤ"), ("jiang", "ㄐㄧㄤ"), ("jiong", "ㄐㄩㄥ"),
("kiang", "ㄎㄧㄤ"), ("kuang", "ㄎㄨㄤ"), ("biang", "ㄅㄧㄤ"), ("duang", "ㄉㄨㄤ"), ("liang", "ㄌㄧㄤ"), ("niang", "ㄋㄧㄤ"),
("qiang", "ㄑㄧㄤ"), ("qiong", "ㄑㄩㄥ"), ("shang", "ㄕㄤ"), ("sheng", "ㄕㄥ"), ("shuai", "ㄕㄨㄞ"), ("shuan", "ㄕㄨㄢ"),
("xiang", "ㄒㄧㄤ"), ("xiong", "ㄒㄩㄥ"), ("zhang", "ㄓㄤ"), ("zheng", "ㄓㄥ"), ("zhong", "ㄓㄨㄥ"), ("zhuai", "ㄓㄨㄞ"),
("zhuan", "ㄓㄨㄢ"), ("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", "ㄗㄨㄢ"), ("jun", "ㄐㄩㄣ"), ("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", "ㄍㄟ"), ("gin", "ㄍㄧㄣ"), ("gen", "ㄍㄣ"), ("gou", "ㄍㄡ"), ("gua", "ㄍㄨㄚ"),
("gue", "ㄍㄨㄜ"), ("gui", "ㄍㄨㄟ"), ("gun", "ㄍㄨㄣ"), ("guo", "ㄍㄨㄛ"), ("hai", "ㄏㄞ"), ("han", "ㄏㄢ"), ("hao", "ㄏㄠ"),
("hei", "ㄏㄟ"), ("hen", "ㄏㄣ"), ("hou", "ㄏㄡ"), ("hua", "ㄏㄨㄚ"), ("hui", "ㄏㄨㄟ"), ("hun", "ㄏㄨㄣ"), ("huo", "ㄏㄨㄛ"),
("jia", "ㄐㄧㄚ"), ("jie", "ㄐㄧㄝ"), ("jin", "ㄐㄧㄣ"), ("jiu", "ㄐㄧㄡ"), ("jue", "ㄐㄩㄝ"), ("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", "ㄗㄨㄛ"), ("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", "ㄗㄨ"), ("a", ""), ("e", ""), ("o", ""), ("q", ""),
("1", " "), ("2", "ˊ"), ("3", "ˇ"), ("4", "ˋ"), ("5", "˙"),
]

View File

@ -34,8 +34,15 @@ public class PopupCompositionBuffer: NSWindowController {
public func sync(accent: NSColor?, locale: String) {
self.locale = locale
self.accent = accent ?? themeColorCocoa
window?.backgroundColor = adjustedThemeColor
if let accent = accent {
self.accent = (accent.alphaComponent == 1) ? accent.withAlphaComponent(Self.bgOpacity) : accent
} else {
self.accent = themeColorCocoa
}
let themeColor = adjustedThemeColor
window?.backgroundColor = .clear
window?.contentView?.layer?.backgroundColor = themeColor.cgColor
window?.contentView?.layer?.borderColor = NSColor.white.withAlphaComponent(0.1).cgColor
messageTextField.backgroundColor = .clear
messageTextField.textColor = textColor
}
@ -44,12 +51,14 @@ public class PopupCompositionBuffer: NSWindowController {
private var locale: String = ""
private static let bgOpacity: CGFloat = 0.8
var themeColorCocoa: NSColor {
switch locale {
case "zh-Hans": return .init(red: 255 / 255, green: 64 / 255, blue: 53 / 255, alpha: 0.85)
case "zh-Hant": return .init(red: 5 / 255, green: 127 / 255, blue: 255 / 255, alpha: 0.85)
case "ja": return .init(red: 167 / 255, green: 137 / 255, blue: 99 / 255, alpha: 0.85)
default: return .init(red: 5 / 255, green: 127 / 255, blue: 255 / 255, alpha: 0.85)
case "zh-Hans": return .init(red: 255 / 255, green: 64 / 255, blue: 53 / 255, alpha: Self.bgOpacity)
case "zh-Hant": return .init(red: 5 / 255, green: 127 / 255, blue: 255 / 255, alpha: Self.bgOpacity)
case "ja": return .init(red: 167 / 255, green: 137 / 255, blue: 99 / 255, alpha: Self.bgOpacity)
default: return .init(red: 5 / 255, green: 127 / 255, blue: 255 / 255, alpha: Self.bgOpacity)
}
}
@ -66,9 +75,8 @@ public class PopupCompositionBuffer: NSWindowController {
)
panel.level = NSWindow.Level(Int(max(CGShieldingWindowLevel(), kCGPopUpMenuWindowLevel)) + 1)
panel.hasShadow = true
panel.backgroundColor = NSColor.controlBackgroundColor
panel.styleMask = .utilityWindow
panel.isMovable = false
panel.backgroundColor = .clear
panel.isOpaque = false
messageTextField = NSTextField()
messageTextField.isEditable = false
messageTextField.isSelectable = false
@ -79,6 +87,15 @@ public class PopupCompositionBuffer: NSWindowController {
messageTextField.font = .systemFont(ofSize: 18) //
panel.contentView?.addSubview(messageTextField)
panel.contentView?.wantsLayer = true
panel.contentView?.shadow = .init()
panel.contentView?.shadow?.shadowBlurRadius = 6
panel.contentView?.shadow?.shadowColor = .black
panel.contentView?.shadow?.shadowOffset = .zero
if let layer = panel.contentView?.layer {
layer.cornerRadius = 9
layer.borderWidth = 1
layer.masksToBounds = true
}
Self.currentWindow = panel
super.init(window: panel)
}
@ -256,6 +273,6 @@ public class PopupCompositionBuffer: NSWindowController {
}
private var adjustedThemeColor: NSColor {
accent.blended(withFraction: NSApplication.isDarkMode ? 0.75 : 0.25, of: .black) ?? accent
accent.blended(withFraction: NSApplication.isDarkMode ? 0.5 : 0.25, of: .black) ?? accent
}
}

View File

@ -13,7 +13,7 @@ let package = Package(
),
],
dependencies: [
.package(path: "../vChewing_CocoaExtension"),
.package(path: "../vChewing_OSFrameworkImpl"),
.package(path: "../vChewing_IMKUtils"),
.package(path: "../vChewing_SwiftExtension"),
],
@ -21,10 +21,14 @@ let package = Package(
.target(
name: "Shared",
dependencies: [
.product(name: "CocoaExtension", package: "vChewing_CocoaExtension"),
.product(name: "OSFrameworkImpl", package: "vChewing_OSFrameworkImpl"),
.product(name: "IMKUtils", package: "vChewing_IMKUtils"),
.product(name: "SwiftExtension", package: "vChewing_SwiftExtension"),
]
),
.testTarget(
name: "SharedTests",
dependencies: ["Shared"]
),
]
)

View File

@ -8,7 +8,7 @@
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import AppKit
import Foundation
@objcMembers public class Broadcaster: NSObject {
public static var shared = Broadcaster()

View File

@ -6,9 +6,7 @@
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import Foundation
public class CandidateNode {
open class CandidateNode {
public var name: String
public var members: [CandidateNode]
public var previous: CandidateNode?

View File

@ -0,0 +1,47 @@
// (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.
public extension CandidateNode {
convenience init(
name: String, services: [CandidateTextService], previous: CandidateNode? = nil
) {
self.init(name: name, members: services.map(\.asCandidateNode), previous: previous)
}
var asServiceMenuNode: ServiceMenuNode? {
self as? ServiceMenuNode
}
var containsCandidateServices: Bool {
!members.compactMap(\.asServiceMenuNode).isEmpty
}
class ServiceMenuNode: CandidateNode {
public var service: CandidateTextService
public init(
name: String, service givenService: CandidateTextService, previous: CandidateNode? = nil
) {
service = givenService
super.init(name: name, previous: previous)
}
}
}
public extension CandidateTextService {
var asCandidateNode: CandidateNode.ServiceMenuNode {
.init(name: menuTitle, service: self)
}
static func getCurrentServiceMenu(
fromMap map: [String]? = nil, candidate: String, reading: [String]
) -> CandidateNode? {
let fetchedRaw = map ?? PrefMgr().candidateServiceMenuContents
let fetched = fetchedRaw.parseIntoCandidateTextServiceStack(candidate: candidate, reading: reading)
return fetched.isEmpty ? nil : .init(name: candidate, services: fetched)
}
}

View File

@ -0,0 +1,106 @@
// (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 struct CandidateTextService: Codable {
public enum ServiceValueType: Int {
case url = 0
case selector = 1
}
public enum ServiceValue: Codable {
case url(URL)
case selector(String)
}
public let key: String
public let reading: [String]
public let menuTitle: String
public let definedValue: String
public let value: ServiceValue
public let candidateText: String
public static var finalSanityCheck: ((CandidateTextService) -> Bool)?
public init?(key: String, definedValue: String, param: String = #"%s"#, reading: [String] = []) {
guard !key.isEmpty, !definedValue.isEmpty, definedValue.first != "#" else { return nil }
candidateText = param
self.key = key.replacingOccurrences(of: #"%s"#, with: param)
self.reading = reading
let rawKeyHasParam = self.key != key
self.definedValue = definedValue.replacingOccurrences(of: #"%s"#, with: param)
// Handle Symbol Menu Title
var newMenuTitle = self.key
if param.count == 1, let strUTFCharCode = param.first?.codePoint, rawKeyHasParam {
newMenuTitle = "\(self.key) (\(strUTFCharCode))"
}
menuTitle = newMenuTitle
// Start parsing rawValue
var temporaryRawValue = definedValue
var finalServiceValue: ServiceValue?
let fetchedTypeHeader = temporaryRawValue.prefix(5)
guard fetchedTypeHeader.count == 5 else { return nil }
for _ in 0 ..< 5 {
temporaryRawValue.removeFirst()
}
switch fetchedTypeHeader.uppercased() {
case #"@SEL:"#:
finalServiceValue = .selector(temporaryRawValue)
case #"@WEB:"#, #"@URL:"#:
let encodedParam = param.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed)
guard let encodedParam = encodedParam else { return nil }
let newURL = URL(string: temporaryRawValue.replacingOccurrences(of: #"%s"#, with: encodedParam))
guard let newURL = newURL else { return nil }
finalServiceValue = .url(newURL)
default: return nil
}
guard let finalServiceValue = finalServiceValue else { return nil }
value = finalServiceValue
let finalSanityCheckResult = Self.finalSanityCheck?(self) ?? true
if !finalSanityCheckResult { return nil }
}
}
extension CandidateTextService: RawRepresentable {
public init?(rawValue: String) {
let cells = rawValue.components(separatedBy: "\t")
guard cells.count == 2 else { return nil }
self.init(key: cells[0], definedValue: cells[1])
}
public var rawValue: String {
"\(key)\t\(definedValue)"
}
public init?(rawValue: String, param: String, reading: [String]) {
let cells = rawValue.components(separatedBy: "\t")
guard cells.count >= 2 else { return nil }
self.init(key: cells[0], definedValue: cells[1], param: param, reading: reading)
}
}
// MARK: - Extensions
public extension Array where Element == CandidateTextService {
var rawRepresentation: [String] {
map(\.rawValue)
}
}
public extension Array where Element == String {
func parseIntoCandidateTextServiceStack(
candidate: String = #"%s"#, reading: [String] = []
) -> [CandidateTextService] {
compactMap { rawValue in
CandidateTextService(rawValue: rawValue, param: candidate, reading: reading)
}
}
}

View File

@ -8,7 +8,6 @@
import AppKit
import Carbon
import Shared
// MARK: - Top-level Enums relating to Input Mode and Language Supports.
@ -52,7 +51,7 @@ public enum IMEApp {
// MARK: -
public static var currentInputMode: Shared.InputMode {
.init(rawValue: PrefMgr.shared.mostRecentInputMode) ?? .imeModeNULL
.init(rawValue: PrefMgr().mostRecentInputMode) ?? .imeModeNULL
}
/// JIS
@ -62,9 +61,10 @@ public enum IMEApp {
/// Fart or Beep?
public static func buzz() {
if PrefMgr.shared.isDebugModeEnabled {
NSSound.buzz(fart: !PrefMgr.shared.shouldNotFartInLieuOfBeep)
} else if !PrefMgr.shared.shouldNotFartInLieuOfBeep {
let prefs = PrefMgr()
if prefs.isDebugModeEnabled {
NSSound.buzz(fart: !prefs.shouldNotFartInLieuOfBeep)
} else if !prefs.shouldNotFartInLieuOfBeep {
NSSound.buzz(fart: true)
} else {
NSSound.beep()

View File

@ -6,14 +6,12 @@
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import AppKit
import Shared
import Foundation
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 = {
@ -27,6 +25,33 @@ 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)
@ -35,6 +60,9 @@ 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
@ -106,6 +134,9 @@ 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
@ -118,6 +149,9 @@ 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
@ -135,9 +169,7 @@ import SwiftExtension
@AppProperty(key: UserDef.kReadingNarrationCoverage.rawValue, defaultValue: 0)
public dynamic var readingNarrationCoverage: Int {
didSet {
SpeechSputnik.shared.refreshStatus()
}
didSet { didAskForRefreshingSpeechSputnik?() }
}
@AppProperty(key: UserDef.kAlsoConfirmAssociatedCandidatesByEnter.rawValue, defaultValue: false)
@ -160,16 +192,12 @@ import SwiftExtension
@AppProperty(key: UserDef.kTogglingAlphanumericalModeWithLShift.rawValue, defaultValue: true)
public dynamic var togglingAlphanumericalModeWithLShift: Bool {
didSet {
SessionCtl.theShiftKeyDetector.toggleWithLShift = togglingAlphanumericalModeWithLShift
}
didSet { didAskForSyncingShiftKeyDetectorPrefs?() }
}
@AppProperty(key: UserDef.kTogglingAlphanumericalModeWithRShift.rawValue, defaultValue: true)
public dynamic var togglingAlphanumericalModeWithRShift: Bool {
didSet {
SessionCtl.theShiftKeyDetector.toggleWithRShift = togglingAlphanumericalModeWithRShift
}
didSet { didAskForSyncingShiftKeyDetectorPrefs?() }
}
@AppProperty(key: UserDef.kConsolidateContextOnCandidateSelection.rawValue, defaultValue: true)
@ -244,23 +272,17 @@ import SwiftExtension
@AppProperty(key: UserDef.kCNS11643Enabled.rawValue, defaultValue: false)
public dynamic var cns11643Enabled: Bool {
didSet {
LMMgr.syncLMPrefs()
}
didSet { didAskForSyncingLMPrefs?() }
}
@AppProperty(key: UserDef.kSymbolInputEnabled.rawValue, defaultValue: true)
public dynamic var symbolInputEnabled: Bool {
didSet {
LMMgr.syncLMPrefs()
}
didSet { didAskForSyncingLMPrefs?() }
}
@AppProperty(key: UserDef.kCassetteEnabled.rawValue, defaultValue: false)
public dynamic var cassetteEnabled: Bool {
didSet {
LMMgr.syncLMPrefs()
}
didSet { didAskForSyncingLMPrefs?() }
}
@AppProperty(key: UserDef.kChineseConversionEnabled.rawValue, defaultValue: false)
@ -319,6 +341,9 @@ 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: "")
@ -337,33 +362,17 @@ import SwiftExtension
@AppProperty(key: UserDef.kUseSCPCTypingMode.rawValue, defaultValue: false)
public dynamic var useSCPCTypingMode: Bool {
didSet {
LMMgr.syncLMPrefs()
}
didSet { didAskForSyncingLMPrefs?() }
}
@AppProperty(key: UserDef.kPhraseReplacementEnabled.rawValue, defaultValue: false)
public dynamic var phraseReplacementEnabled: Bool {
didSet {
LMMgr.syncLMPrefs()
}
willSet {
if newValue {
LMMgr.loadUserPhraseReplacement()
}
}
didSet { didAskForSyncingLMPrefs?() }
}
@AppProperty(key: UserDef.kAssociatedPhrasesEnabled.rawValue, defaultValue: false)
public dynamic var associatedPhrasesEnabled: Bool {
didSet {
LMMgr.syncLMPrefs()
}
willSet {
if newValue {
LMMgr.loadUserAssociatesData()
}
}
didSet { didAskForSyncingLMPrefs?() }
}
// MARK: - Keyboard HotKey Enable / Disable

View File

@ -7,28 +7,16 @@
// 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? {
let excluded = useJKtoMoveCompositorCursorInCandidateState ? "jk" : ""
var excluded = ""
if useJKtoMoveCompositorCursorInCandidateState { excluded.append("jk") }
if useShiftQuestionToCallServiceMenu { excluded.append("?") }
excluded.append(IMEApp.isKeyboardJIS ? "_" : "`~")
return CandidateKey.validate(keys: candidateKeys, excluding: excluded)
}
}
@ -80,6 +68,9 @@ public extension PrefMgr {
if ![0, 1, 2].contains(readingNarrationCoverage) {
readingNarrationCoverage = 0
}
if ![0, 1, 2, 3].contains(specifyCmdOptCtrlEnterBehavior) {
specifyCmdOptCtrlEnterBehavior = 0
}
}
}
@ -87,7 +78,7 @@ public extension PrefMgr {
public extension PrefMgr {
@discardableResult func dumpShellScriptBackup() -> String? {
let mirror = Mirror(reflecting: PrefMgr.shared)
let mirror = Mirror(reflecting: self)
guard let bundleIdentifier = Bundle.main.bundleIdentifier else { return nil }
let strDoubleDashLine = String(String(repeating: "=", count: 70))
let consoleOutput = NSMutableString(string: "#!/bin/sh\n\n")

View File

@ -7,7 +7,7 @@
// requirements defined in MIT License.
import AppKit
import CocoaExtension
import OSFrameworkImpl
// MARK: - InputSignalProtocol

View File

@ -11,6 +11,7 @@ import Foundation
public protocol PrefMgrProtocol {
var isDebugModeEnabled: Bool { get set }
var failureFlagForUOMObservation: Bool { get set }
var candidateServiceMenuContents: [String] { get set }
var respectClientAccentColor: Bool { get set }
var securityHardenedCompositionBuffer: Bool { get set }
var checkAbusersOfSecureEventInputAPI: Bool { get set }
@ -31,10 +32,12 @@ public protocol PrefMgrProtocol {
var shouldAutoReloadUserDataFiles: Bool { get set }
var useRearCursorMode: Bool { get set }
var useJKtoMoveCompositorCursorInCandidateState: Bool { get set }
var useShiftQuestionToCallServiceMenu: Bool { get set }
var moveCursorAfterSelectingCandidate: Bool { get set }
var dodgeInvalidEdgeCandidateCursorPosition: Bool { get set }
var useDynamicCandidateWindowOrigin: Bool { get set }
var useHorizontalCandidateList: Bool { get set }
var minCellWidthForHorizontalMatrix: Int { get set }
var chooseCandidateUsingSpace: Bool { get set }
var allowBoostingSingleKanjiAsUserPhrase: Bool { get set }
var fetchSuggestionsFromUserOverrideModel: Bool { get set }
@ -83,6 +86,7 @@ public protocol PrefMgrProtocol {
var specifyShiftBackSpaceKeyBehavior: Int { get set }
var specifyShiftTabKeyBehavior: Bool { get set }
var specifyShiftSpaceKeyBehavior: Bool { get set }
var specifyCmdOptCtrlEnterBehavior: Int { get set }
var candidateTextFontName: String { get set }
var candidateKeys: String { get set }
var useSCPCTypingMode: Bool { get set }

View File

@ -274,12 +274,13 @@ public enum Shared {
// MARK: - PEReloadEventObserver
@available(macOS 10.15, *)
public class PEReloadEventObserver: ObservableObject, Equatable {
public class PEReloadEventObserver: NSObject, ObservableObject {
public static let shared = PEReloadEventObserver()
private var observation: NSKeyValueObservation?
@Published public var id = UUID().uuidString
public init() {
override public init() {
super.init()
observation = Broadcaster.shared.observe(\.eventForReloadingPhraseEditor, options: [.new]) { _, _ in
self.touch()
}

View File

@ -33,6 +33,7 @@ public enum UserDef: String, CaseIterable, Identifiable {
case kIsDebugModeEnabled = "_DebugMode"
case kFailureFlagForUOMObservation = "_FailureFlag_UOMObservation"
case kCandidateServiceMenuContents = "CandidateServiceMenuContents"
case kRespectClientAccentColor = "RespectClientAccentColor"
case kSecurityHardenedCompositionBuffer = "SecurityHardenedCompositionBuffer"
case kCheckAbusersOfSecureEventInputAPI = "CheckAbusersOfSecureEventInputAPI"
@ -53,8 +54,10 @@ public enum UserDef: String, CaseIterable, Identifiable {
case kShouldAutoReloadUserDataFiles = "ShouldAutoReloadUserDataFiles"
case kUseRearCursorMode = "UseRearCursorMode"
case kUseJKtoMoveCompositorCursorInCandidateState = "UseJKtoMoveCompositorCursorInCandidateState"
case kUseShiftQuestionToCallServiceMenu = "UseShiftQuestionToCallServiceMenu"
case kUseDynamicCandidateWindowOrigin = "UseDynamicCandidateWindowOrigin"
case kUseHorizontalCandidateList = "UseHorizontalCandidateList"
case kMinCellWidthForHorizontalMatrix = "MinCellWidthForHorizontalMatrix"
case kChooseCandidateUsingSpace = "ChooseCandidateUsingSpace"
case kCassetteEnabled = "CassetteEnabled"
case kCNS11643Enabled = "CNS11643Enabled"
@ -71,6 +74,7 @@ public enum UserDef: String, CaseIterable, Identifiable {
case kSpecifyShiftBackSpaceKeyBehavior = "SpecifyShiftBackSpaceKeyBehavior"
case kSpecifyShiftTabKeyBehavior = "SpecifyShiftTabKeyBehavior"
case kSpecifyShiftSpaceKeyBehavior = "SpecifyShiftSpaceKeyBehavior"
case kSpecifyCmdOptCtrlEnterBehavior = "SpecifyCmdOptCtrlEnterBehavior"
case kAllowBoostingSingleKanjiAsUserPhrase = "AllowBoostingSingleKanjiAsUserPhrase"
case kUseSCPCTypingMode = "UseSCPCTypingMode"
case kMaxCandidateLength = "MaxCandidateLength"
@ -157,6 +161,7 @@ public extension UserDef {
switch self {
case .kIsDebugModeEnabled: return .bool
case .kFailureFlagForUOMObservation: return .bool
case .kCandidateServiceMenuContents: return .dictionary
case .kRespectClientAccentColor: return .bool
case .kSecurityHardenedCompositionBuffer: return .bool
case .kCheckAbusersOfSecureEventInputAPI: return .bool
@ -177,8 +182,10 @@ public extension UserDef {
case .kShouldAutoReloadUserDataFiles: return .bool
case .kUseRearCursorMode: return .bool
case .kUseJKtoMoveCompositorCursorInCandidateState: return .bool
case .kUseShiftQuestionToCallServiceMenu: return .bool
case .kUseDynamicCandidateWindowOrigin: return .bool
case .kUseHorizontalCandidateList: return .bool
case .kMinCellWidthForHorizontalMatrix: return .integer
case .kChooseCandidateUsingSpace: return .bool
case .kCassetteEnabled: return .bool
case .kCNS11643Enabled: return .bool
@ -195,6 +202,7 @@ public extension UserDef {
case .kSpecifyShiftBackSpaceKeyBehavior: return .integer
case .kSpecifyShiftTabKeyBehavior: return .bool
case .kSpecifyShiftSpaceKeyBehavior: return .bool
case .kSpecifyCmdOptCtrlEnterBehavior: return .integer
case .kAllowBoostingSingleKanjiAsUserPhrase: return .bool
case .kUseSCPCTypingMode: return .bool
case .kMaxCandidateLength: return .integer
@ -252,6 +260,7 @@ public extension UserDef {
var metaData: MetaData? {
switch self {
case .kIsDebugModeEnabled: return .init(userDef: self, shortTitle: "Debug Mode")
case .kCandidateServiceMenuContents: return nil
case .kFailureFlagForUOMObservation: return nil
case .kRespectClientAccentColor: return .init(
userDef: self, shortTitle: "i18n:userdef.kRespectClientAccentColor.shortTitle",
@ -339,6 +348,10 @@ public extension UserDef {
shortTitle: "i18n:UserDef.kUseJKtoMoveCompositorCursorInCandidateState.shortTitle",
description: "i18n:UserDef.kUseJKtoMoveCompositorCursorInCandidateState.description"
)
case .kUseShiftQuestionToCallServiceMenu: return .init(
userDef: self,
shortTitle: "i18n:UserDef.kUseShiftQuestionToCallServiceMenu.shortTitle"
)
case .kUseDynamicCandidateWindowOrigin: return .init(
userDef: self, shortTitle: "Adjust candidate window location according to current node length"
)
@ -347,6 +360,13 @@ public extension UserDef {
description: "Choose your preferred layout of the candidate window.",
options: [0: "Vertical", 1: "Horizontal"]
)
case .kMinCellWidthForHorizontalMatrix: return .init(
userDef: self, shortTitle: "i18n:userdef.kMinCellWidthForHorizontalMatrix.shortTitle",
options: [
0: "i18n:UserDef.kMinCellWidthForHorizontalMatrix.option.0",
1: "i18n:UserDef.kMinCellWidthForHorizontalMatrix.option.1",
]
)
case .kChooseCandidateUsingSpace: return .init(
userDef: self, shortTitle: "Enable Space key for calling candidate window",
description: "If disabled, this will insert space instead."
@ -415,6 +435,16 @@ public extension UserDef {
1: "Space to +revolve pages, Shift+Space to +revolve candidates",
]
)
case .kSpecifyCmdOptCtrlEnterBehavior: return .init(
userDef: self, shortTitle: "i18n:UserDef.kSpecifyCmdOptCtrlEnterBehavior.shortTitle",
description: "i18n:UserDef.kSpecifyCmdOptCtrlEnterBehavior.description",
options: [
0: "i18n:UserDef.kSpecifyCmdOptCtrlEnterBehavior.option.0",
1: "i18n:UserDef.kSpecifyCmdOptCtrlEnterBehavior.option.1",
2: "i18n:UserDef.kSpecifyCmdOptCtrlEnterBehavior.option.2",
3: "i18n:UserDef.kSpecifyCmdOptCtrlEnterBehavior.option.3",
]
)
case .kAllowBoostingSingleKanjiAsUserPhrase: return .init(
userDef: self, shortTitle: "Allow boosting / excluding a candidate of single kanji when marking",
description: "⚠︎ This may hinder the walking algorithm from giving appropriate results."
@ -432,7 +462,8 @@ public extension UserDef {
userDef: self, shortTitle: "Show Hanyu-Pinyin in the inline composition buffer"
)
case .kInlineDumpPinyinInLieuOfZhuyin: return .init(
userDef: self, shortTitle: "Commit Hanyu-Pinyin instead on Ctrl(+Option)+Command+Enter"
userDef: self, shortTitle: "Commit Hanyu-Pinyin instead on Ctrl(+Option)+Command+Enter",
description: "i18n:UserDef.kInlineDumpPinyinInLieuOfZhuyin.description"
)
case .kFetchSuggestionsFromUserOverrideModel: return .init(
userDef: self, shortTitle: "Applying typing suggestions from half-life user override model",

View File

@ -9,9 +9,9 @@
// UserDefRenderable AppKit SwiftUI
import AppKit
import CocoaExtension
import Foundation
import IMKUtils
import OSFrameworkImpl
public class UserDefRenderableCocoa: NSObject, Identifiable {
public let def: UserDef

View File

@ -0,0 +1,48 @@
// (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 Shared
import XCTest
final class SharedTests: XCTestCase {
// MARK: - PrefMgr().dumpShellScriptBackup()
func testDumpedPrefs() throws {
let prefs = PrefMgr()
let fetched = prefs.dumpShellScriptBackup() ?? ""
XCTAssertFalse(fetched.isEmpty)
}
// MARK: - CandidateTextService (Basic Tests)
static let testDataMap: [String] = [
#"Bing: %s"# + "\t" + #"@WEB:https://www.bing.com/search?q=%s"#,
#"Ecosia: %s"# + "\t" + #"@WEB:https://www.ecosia.org/search?method=index&q=%s"#,
]
func testDataRestoration() throws {
let stacked = Self.testDataMap.parseIntoCandidateTextServiceStack()
stacked.forEach { currentService in
print(currentService)
}
XCTAssertEqual(stacked.rawRepresentation, Self.testDataMap)
}
func testCandidateServiceMenuNode() throws {
let rootNode = CandidateTextService.getCurrentServiceMenu(
fromMap: Self.testDataMap,
candidate: "🍰", reading: ["ㄉㄢˋ", "ㄍㄠ"]
)
guard let rootNode = rootNode else {
XCTAssertThrowsError("Root Node Construction Failed.")
return
}
print(rootNode.members.map(\.name))
print(rootNode.members.compactMap(\.asServiceMenuNode?.service))
}
}

View File

@ -6,8 +6,6 @@
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import Foundation
// MARK: - Bool Operators
public func |= (lhs: inout Bool, rhs: Bool) {
@ -22,25 +20,11 @@ public func ^= (lhs: inout Bool, rhs: Bool) {
lhs = lhs != rhs
}
// MARK: - String.localized extension
public extension StringLiteralType {
var localized: String { NSLocalizedString(description, comment: "") }
}
// MARK: - Root Extensions
// MARK: - Root Extensions (deduplicated)
// Extend the RangeReplaceableCollection to allow it clean duplicated characters.
// Ref: https://stackoverflow.com/questions/25738817/
public extension RangeReplaceableCollection where Element: Hashable {
/// 使 NSOrderedSet class
var classDeduplicated: Self {
NSOrderedSet(array: Array(self)).compactMap { $0 as? Element.Type } as? Self ?? self
// Bug KeyValuePaired
// var set = Set<Element>()
// return filter { set.insert($0).inserted }
}
///
/// - Remark: class class Identifiable
var deduplicated: Self {
@ -49,22 +33,6 @@ public extension RangeReplaceableCollection where Element: Hashable {
}
}
// MARK: - String Tildes Expansion Extension
public extension String {
var expandingTildeInPath: String {
(self as NSString).expandingTildeInPath
}
}
// MARK: - String Localized Error Extension
extension String: LocalizedError {
public var errorDescription: String? {
self
}
}
// MARK: - Ensuring trailing slash of a string
public extension String {
@ -77,21 +45,6 @@ public extension String {
// MARK: - CharCode printability check
// Ref: https://forums.swift.org/t/57085/5
public extension UniChar {
var isPrintable: Bool {
guard Unicode.Scalar(UInt32(self)) != nil else {
struct NotAWholeScalar: Error {}
return false
}
return true
}
var isPrintableASCII: Bool {
(32 ... 126).contains(self)
}
}
public extension Unicode.Scalar {
var isPrintableASCII: Bool {
(32 ... 126).contains(value)
@ -133,74 +86,27 @@ public extension Bool {
}
}
// MARK: - User Defaults Storage
public extension UserDefaults {
//
static var pendingUnitTests = false
static var unitTests = UserDefaults(suiteName: "UnitTests")
static var current: UserDefaults {
pendingUnitTests ? .unitTests ?? .standard : .standard
}
}
// MARK: - Property Wrapper
// Ref: https://www.avanderlee.com/swift/property-wrappers/
@propertyWrapper
public struct AppProperty<Value> {
public let key: String
public let defaultValue: Value
public var container: UserDefaults { .current }
public init(key: String, defaultValue: Value) {
self.key = key
self.defaultValue = defaultValue
if container.object(forKey: key) == nil {
container.set(defaultValue, forKey: key)
}
}
public var wrappedValue: Value {
get {
container.object(forKey: key) as? Value ?? defaultValue
}
set {
container.set(newValue, forKey: key)
}
}
}
// MARK: -
// Ref: https://stackoverflow.com/a/32581409/4162914
public extension Double {
func rounded(toPlaces places: Int) -> Double {
let divisor = pow(10.0, Double(places))
let divisor = 10.0.mathPowered(by: places)
return (self * divisor).rounded() / divisor
}
}
// MARK: - String RegReplace Extension
// Ref: https://stackoverflow.com/a/40993403/4162914 && https://stackoverflow.com/a/71291137/4162914
public extension String {
mutating func regReplace(pattern: String, replaceWith: String = "") {
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 }
public extension Double {
func mathPowered(by operand: Int) -> Double {
var target = self
for _ in 0 ..< operand {
target = target * target
}
return target
}
}
// MARK: - String CharName Extension
// MARK: - String CharName and CodePoint Extension
public extension String {
var charDescriptions: [String] {
@ -209,6 +115,25 @@ public extension String {
return String(format: "U+%02X %@", $0.value, theName)
}
}
var codePoints: [String] {
map(\.codePoint)
}
var describedAsCodePoints: [String] {
map {
"\($0) (\($0.codePoint))"
}
}
}
// MARK: - Character Codepoint
public extension Character {
var codePoint: String {
guard let value = unicodeScalars.first?.value else { return "U+NULL" }
return String(format: "U+%02X", value)
}
}
// MARK: - String Ellipsis Extension
@ -217,26 +142,6 @@ public extension String {
var withEllipsis: String { self + "" }
}
// MARK: - Localized String Extension for Integers and Floats
public extension BinaryFloatingPoint {
func i18n(loc: String) -> String {
let formatter = NumberFormatter()
formatter.locale = Locale(identifier: loc)
formatter.numberStyle = .spellOut
return formatter.string(from: NSDecimalNumber(string: "\(self)")) ?? ""
}
}
public extension BinaryInteger {
func i18n(loc: String) -> String {
let formatter = NumberFormatter()
formatter.locale = Locale(identifier: loc)
formatter.numberStyle = .spellOut
return formatter.string(from: NSDecimalNumber(string: "\(self)")) ?? ""
}
}
// MARK: - Index Revolver (only for Array)
// Further discussion: https://forums.swift.org/t/62847
@ -264,31 +169,6 @@ public extension Int {
}
}
// MARK: - Parse String As Hex Literal
// Original author: Shiki Suen
// Refactored by: Isaac Xen
public extension String {
func parsedAsHexLiteral(encoding: CFStringEncodings? = nil) -> String? {
guard !isEmpty else { return nil }
var charBytes = [Int8]()
var buffer: Int?
compactMap(\.hexDigitValue).forEach { neta in
if let validBuffer = buffer {
charBytes.append(.init(bitPattern: UInt8(validBuffer << 4 + neta)))
buffer = nil
} else {
buffer = neta
}
}
let encodingUBE = CFStringBuiltInEncodings.UTF16BE.rawValue
let encodingRAW = encoding.map { UInt32($0.rawValue) } ?? encodingUBE
let result = CFStringCreateWithCString(nil, &charBytes, encodingRAW) as String?
return result?.isEmpty ?? true ? nil : result
}
}
// MARK: - Overlap Checker (for two sets)
public extension Set where Element: Hashable {
@ -321,32 +201,6 @@ public extension Array where Element: Hashable {
}
}
// MARK: - Version Comparer.
public extension String {
/// ref: https://sarunw.com/posts/how-to-compare-two-app-version-strings-in-swift/
func versionCompare(_ otherVersion: String) -> ComparisonResult {
let versionDelimiter = "."
var versionComponents = components(separatedBy: versionDelimiter) // <1>
var otherVersionComponents = otherVersion.components(separatedBy: versionDelimiter)
let zeroDiff = versionComponents.count - otherVersionComponents.count // <2>
// <3> Compare normally if the formats are the same.
guard zeroDiff != 0 else { return compare(otherVersion, options: .numeric) }
let zeros = Array(repeating: "0", count: abs(zeroDiff)) // <4>
if zeroDiff > 0 {
otherVersionComponents.append(contentsOf: zeros) // <5>
} else {
versionComponents.append(contentsOf: zeros)
}
return versionComponents.joined(separator: versionDelimiter)
.compare(otherVersionComponents.joined(separator: versionDelimiter), options: .numeric) // <6>
}
}
// MARK: - Array Builder.
@resultBuilder
@ -379,3 +233,54 @@ public enum ArrayBuilder<OutputModel> {
Array(components.joined())
}
}
// MARK: - Extending Comparable to let it able to find its neighbor values in any collection.
public extension Comparable {
func findNeighborValue(from givenSeq: any Collection<Self>, greater isGreater: Bool) -> Self? {
let givenArray: [Self] = isGreater ? Array(givenSeq.sorted()) : Array(givenSeq.sorted().reversed())
let givenMap: [Int: Self] = .init(uniqueKeysWithValues: Array(givenArray.enumerated()))
var (startID, endID, returnableID) = (0, givenArray.count - 1, -1)
func internalCompare(_ lhs: Self, _ rhs: Self) -> Bool { isGreater ? lhs <= rhs : lhs >= rhs }
while let startObj = givenMap[startID], let endObj = givenMap[endID], internalCompare(startObj, endObj) {
let midID = (startID + endID) / 2
if let midObj = givenMap[midID], internalCompare(midObj, self) {
startID = midID + 1
} else {
returnableID = midID
endID = midID - 1
}
}
return givenMap[returnableID]
}
}
// MARK: - String.applyingTransform
public extension String {
/// This only works with ASCII chars for now.
func applyingTransformFW2HW(reverse: Bool) -> String {
var arr: [Character] = map { $0 }
for i in 0 ..< arr.count {
let oldChar = arr[i]
guard oldChar.unicodeScalars.count == 1 else { continue }
guard let oldCodePoint = oldChar.unicodeScalars.first?.value else { continue }
if reverse {
guard oldChar.isASCII else { continue }
} else {
guard oldCodePoint > 0xFEE0 || oldCodePoint == 0x3000 else { continue }
}
var newCodePoint: Int32 = reverse ? (Int32(oldCodePoint) + 0xFEE0) : (Int32(oldCodePoint) - 0xFEE0)
checkSpace: switch (oldCodePoint, reverse) {
case (0x3000, false): newCodePoint = 0x20
case (0x20, true): newCodePoint = 0x3000
default: break checkSpace
}
guard newCodePoint > 0 else { continue }
guard let newScalar = Unicode.Scalar(UInt16(newCodePoint)) else { continue }
let newChar = Character(newScalar)
arr[i] = newChar
}
return String(arr)
}
}

View File

@ -0,0 +1,190 @@
// (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
// MARK: - String.localized extension
public extension StringLiteralType {
var localized: String { NSLocalizedString(description, comment: "") }
}
// MARK: - Root Extensions (classDeduplicated)
// Extend the RangeReplaceableCollection to allow it clean duplicated characters.
// Ref: https://stackoverflow.com/questions/25738817/
public extension RangeReplaceableCollection where Element: Hashable {
/// 使 NSOrderedSet class
var classDeduplicated: Self {
NSOrderedSet(array: Array(self)).compactMap { $0 as? Element.Type } as? Self ?? self
// Bug KeyValuePaired
// var set = Set<Element>()
// return filter { set.insert($0).inserted }
}
}
// MARK: - String Tildes Expansion Extension
public extension String {
var expandingTildeInPath: String {
(self as NSString).expandingTildeInPath
}
}
// MARK: - String Localized Error Extension
extension String: LocalizedError {
public var errorDescription: String? {
self
}
}
// MARK: - CharCode printability check for UniChar (CoreFoundation)
// Ref: https://forums.swift.org/t/57085/5
public extension UniChar {
var isPrintable: Bool {
guard Unicode.Scalar(UInt32(self)) != nil else {
struct NotAWholeScalar: Error {}
return false
}
return true
}
var isPrintableASCII: Bool {
(32 ... 126).contains(self)
}
}
// MARK: - User Defaults Storage
public extension UserDefaults {
//
static var pendingUnitTests = false
static var unitTests = UserDefaults(suiteName: "UnitTests")
static var current: UserDefaults {
pendingUnitTests ? .unitTests ?? .standard : .standard
}
}
// MARK: - Property Wrapper
// Ref: https://www.avanderlee.com/swift/property-wrappers/
@propertyWrapper
public struct AppProperty<Value> {
public let key: String
public let defaultValue: Value
public var container: UserDefaults { .current }
public init(key: String, defaultValue: Value) {
self.key = key
self.defaultValue = defaultValue
if container.object(forKey: key) == nil {
container.set(defaultValue, forKey: key)
}
}
public var wrappedValue: Value {
get {
container.object(forKey: key) as? Value ?? defaultValue
}
set {
container.set(newValue, forKey: key)
}
}
}
// MARK: - String RegReplace Extension
// Ref: https://stackoverflow.com/a/40993403/4162914 && https://stackoverflow.com/a/71291137/4162914
public extension String {
mutating func regReplace(pattern: String, replaceWith: String = "") {
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 }
}
}
// MARK: - Localized String Extension for Integers and Floats
public extension BinaryFloatingPoint {
func i18n(loc: String) -> String {
let formatter = NumberFormatter()
formatter.locale = Locale(identifier: loc)
formatter.numberStyle = .spellOut
return formatter.string(from: NSDecimalNumber(string: "\(self)")) ?? ""
}
}
public extension BinaryInteger {
func i18n(loc: String) -> String {
let formatter = NumberFormatter()
formatter.locale = Locale(identifier: loc)
formatter.numberStyle = .spellOut
return formatter.string(from: NSDecimalNumber(string: "\(self)")) ?? ""
}
}
// MARK: - Parse String As Hex Literal
// Original author: Shiki Suen
// Refactored by: Isaac Xen
public extension String {
func parsedAsHexLiteral(encoding: CFStringEncodings? = nil) -> String? {
guard !isEmpty else { return nil }
var charBytes = [Int8]()
var buffer: Int?
compactMap(\.hexDigitValue).forEach { neta in
if let validBuffer = buffer {
charBytes.append(.init(bitPattern: UInt8(validBuffer << 4 + neta)))
buffer = nil
} else {
buffer = neta
}
}
let encodingUBE = CFStringBuiltInEncodings.UTF16BE.rawValue
let encodingRAW = encoding.map { UInt32($0.rawValue) } ?? encodingUBE
let result = CFStringCreateWithCString(nil, &charBytes, encodingRAW) as String?
return result?.isEmpty ?? true ? nil : result
}
}
// MARK: - Version Comparer.
public extension String {
/// ref: https://sarunw.com/posts/how-to-compare-two-app-version-strings-in-swift/
func versionCompare(_ otherVersion: String) -> ComparisonResult {
let versionDelimiter = "."
var versionComponents = components(separatedBy: versionDelimiter) // <1>
var otherVersionComponents = otherVersion.components(separatedBy: versionDelimiter)
let zeroDiff = versionComponents.count - otherVersionComponents.count // <2>
// <3> Compare normally if the formats are the same.
guard zeroDiff != 0 else { return compare(otherVersion, options: .numeric) }
let zeros = Array(repeating: "0", count: abs(zeroDiff)) // <4>
if zeroDiff > 0 {
otherVersionComponents.append(contentsOf: zeros) // <5>
} else {
versionComponents.append(contentsOf: zeros)
}
return versionComponents.joined(separator: versionDelimiter)
.compare(otherVersionComponents.joined(separator: versionDelimiter), options: .numeric) // <6>
}
}

View File

@ -4,7 +4,7 @@ import PackageDescription
let package = Package(
name: "Tekkon",
platforms: [
.macOS(.v11),
.macOS(.v10_11),
],
products: [
.library(

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