Skip to content

Commit eb80b16

Browse files
authored
Merge pull request openvanilla#873 from zonble/char_info
Make it able to view more character information.
2 parents 14f672c + f331f87 commit eb80b16

9 files changed

Lines changed: 211 additions & 4 deletions

File tree

Packages/SystemCharacterInfo/Sources/SystemCharacterInfo/SystemCharacterInfo.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ public enum SystemCharacterInfoError : Error, LocalizedError {
4646
}
4747

4848
public class SystemCharacterInfo {
49+
public static let shared: SystemCharacterInfo? = try? SystemCharacterInfo()
50+
4951
private var db: Connection
5052

5153
public init() throws {
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright (c) 2026 and onwards The McBopomofo Authors.
2+
//
3+
// Permission is hereby granted, free of charge, to any person
4+
// obtaining a copy of this software and associated documentation
5+
// files (the "Software"), to deal in the Software without
6+
// restriction, including without limitation the rights to use,
7+
// copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
// copies of the Software, and to permit persons to whom the
9+
// Software is furnished to do so, subject to the following
10+
// conditions:
11+
//
12+
// The above copyright notice and this permission notice shall be
13+
// included in all copies or substantial portions of the Software.
14+
//
15+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16+
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17+
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18+
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19+
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20+
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21+
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22+
// OTHER DEALINGS IN THE SOFTWARE.
23+
24+
25+
import SQLite
26+
import Foundation
27+
28+
private let path = "/System/Library/Input Methods/CharacterPalette.app/Contents/Resources/CharacterDB.sqlite3"
29+
30+
public struct UnihanEntry {
31+
public var name: String
32+
public var japanese: String
33+
/// 日文訓讀
34+
public var japaneseKun: String
35+
/// 日文音讀
36+
public var japaneseOn: String
37+
public var korean: String
38+
public var pinyinPrc: String
39+
public var pinyinRoc: String
40+
public var pinyinPrimary: String
41+
public var canjie: String
42+
public var canjieKeys: String
43+
public var components: String
44+
public var phonetic: String
45+
/// 五筆形
46+
public var wubiXing: String
47+
/// 五筆劃
48+
public var wubiHua: String
49+
}
50+
51+
public class UnihanDictionary {
52+
public static let shared: UnihanDictionary? = try? UnihanDictionary()
53+
54+
class UnihanEntryParser {
55+
static func parse(from string: String) -> UnihanEntry {
56+
let components = string.split(separator: "|", omittingEmptySubsequences: false)
57+
func get(_ index: Int) -> String {
58+
return index < components.count ? String(components[index]) : ""
59+
}
60+
return UnihanEntry(
61+
name: get(0),
62+
japanese: get(1),
63+
japaneseKun: get(12),
64+
japaneseOn: get(13),
65+
korean: get(2),
66+
pinyinPrc: get(3),
67+
pinyinRoc: get(10),
68+
pinyinPrimary: get(14),
69+
canjie: get(8),
70+
canjieKeys: get(15),
71+
components: get(7),
72+
phonetic: get(11),
73+
wubiXing: get(4),
74+
wubiHua: get(5)
75+
)
76+
}
77+
}
78+
79+
private var db: Connection
80+
81+
public init() throws {
82+
self.db = try Connection(path)
83+
}
84+
85+
public func read(string: String) throws -> UnihanEntry {
86+
let table = Table("unihan_dict")
87+
let uchr = Expression<String>("uchr")
88+
let query = table.filter(uchr == string).limit(1)
89+
let result = try db.prepare(query)
90+
let rows = Array(result)
91+
guard let row = rows.first else {
92+
throw SystemCharacterInfoError.notFound
93+
}
94+
let info = SQLite.Expression<String?>("info")
95+
let infoString = try? row.get(info)
96+
guard let infoString else {
97+
throw SystemCharacterInfoError.notFound
98+
}
99+
let entry = UnihanEntryParser.parse(from: infoString)
100+
return entry
101+
}
102+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright (c) 2026 and onwards The McBopomofo Authors.
2+
//
3+
// Permission is hereby granted, free of charge, to any person
4+
// obtaining a copy of this software and associated documentation
5+
// files (the "Software"), to deal in the Software without
6+
// restriction, including without limitation the rights to use,
7+
// copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
// copies of the Software, and to permit persons to whom the
9+
// Software is furnished to do so, subject to the following
10+
// conditions:
11+
//
12+
// The above copyright notice and this permission notice shall be
13+
// included in all copies or substantial portions of the Software.
14+
//
15+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16+
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17+
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18+
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19+
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20+
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21+
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22+
// OTHER DEALINGS IN THE SOFTWARE.
23+
24+
import Testing
25+
@testable import SystemCharacterInfo
26+
27+
@Test
28+
func testUnihanDictionary1() async throws {
29+
let dict = try UnihanDictionary()
30+
let result = try dict.read(string: "")
31+
#expect(result.canjie == "卜一")
32+
#expect(result.canjieKeys == "YM")
33+
}
34+
35+
@Test
36+
func testUnihanDictionary2() async throws {
37+
let dict = try UnihanDictionary()
38+
let result = try dict.read(string: "")
39+
#expect(result.canjie == "戈口")
40+
#expect(result.canjieKeys == "IR")
41+
}
42+
43+
@Test
44+
func testUnihanDictionary3() async throws {
45+
let dict = try UnihanDictionary()
46+
let result = try dict.read(string: "")
47+
#expect(result.canjie == "大大大大")
48+
#expect(result.canjieKeys == "KKKK")
49+
}

Source/Base.lproj/Localizable.strings

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,13 @@
178178
"Cannot add new phrases when Bopomofo annotation is on" = "Cannot add new phrases when Bopomofo annotation is on";
179179

180180
"Add Variant Annotation (Need BPMF IVS Font Support)" = "Add Variant Annotation (Need BPMF IVS Font Support)";
181+
182+
"Unicode Name" = "Unicode Name";
183+
"Phoenetic" = "Phoenetic";
184+
"Pinyin" = "Pinyin";
185+
"Canjie" = "Canjie";
186+
"Canjie Keys" = "Canjie Keys";
187+
"Japanese" = "Japanese";
188+
"Japanese Kun" = "Japanese Kun";
189+
"Japanese On" = "Japanese On";
190+
"Korean" = "Korean";

Source/InputMethodController+CandidateControllerDelegate.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,11 +215,11 @@ extension McBopomofoInputMethodController: CandidateControllerDelegate {
215215
}
216216

217217
func checkIfSystemCharacterInfoReady() -> Bool {
218-
charInfo != nil
218+
SystemCharacterInfo.shared != nil
219219
}
220220

221221
func getSystemExplanation(for chr: String) -> String? {
222-
guard let charInfo = charInfo,
222+
guard let charInfo = SystemCharacterInfo.shared,
223223
let result = try? charInfo.read(string: chr) else {
224224
return nil
225225
}

Source/InputMethodController.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ class McBopomofoInputMethodController: IMKInputController {
5454
var currentClient: Any?
5555
var keyHandler: KeyHandler = KeyHandler()
5656
var state: InputState = InputState.Empty()
57-
lazy var charInfo: SystemCharacterInfo? = try? SystemCharacterInfo()
5857

5958
// Share the stored issues, so a set of issues is shown as notification only once.
6059
static var latestUserFileIssues: [String] = []

Source/InputState.swift

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import Cocoa
2525
import NSStringUtils
26+
import SystemCharacterInfo
2627

2728
@objc protocol CandidateProvider: NSObjectProtocol {
2829
@objc var candidateCount: Int { get }
@@ -771,7 +772,7 @@ class InputState: NSObject {
771772
}.joined(separator: " ")
772773
}
773774

774-
self.menuTitleValueMapping = [
775+
var menuTitleValueMapping = [
775776
buildItem(
776777
prefix: "UTF-8 HEX", selectedString: selectedPhrase,
777778
builder: { string in
@@ -807,6 +808,30 @@ class InputState: NSObject {
807808
getCharCode(string: string, encoding: 0x0A01)
808809
}),
809810
]
811+
if selectedString.count == 1,
812+
let dictionary = UnihanDictionary.shared,
813+
let result = try? dictionary.read(string: selectedString) {
814+
let mapping: [(String, String?)] = [
815+
(NSLocalizedString("Unicode Name", comment:""), result.name),
816+
(NSLocalizedString("Phoenetic", comment:""), result.phonetic),
817+
(NSLocalizedString("Pinyin", comment:""), result.pinyinRoc),
818+
(NSLocalizedString("Canjie", comment:""), result.canjie),
819+
(NSLocalizedString("Canjie Keys", comment:""), result.canjieKeys),
820+
(NSLocalizedString("Japanese", comment:"") , result.japanese),
821+
(NSLocalizedString("Japanese Kun", comment:""), result.japaneseKun),
822+
(NSLocalizedString("Japanese On", comment:""), result.japaneseOn),
823+
(NSLocalizedString("Korean", comment:""), result.korean),
824+
]
825+
for entry in mapping {
826+
if let string = entry.1, !string.isEmpty {
827+
let truncated = string.count > 16 ? String(
828+
string[string.startIndex..<string.index(string.startIndex, offsetBy: 16)]
829+
) + "" : string
830+
menuTitleValueMapping.append(("\(entry.0): \(truncated)", string))
831+
}
832+
}
833+
}
834+
self.menuTitleValueMapping = menuTitleValueMapping
810835
self.menu = menuTitleValueMapping.map { $0.0 }
811836
super.init(
812837
composingBuffer: previousState.composingBuffer,

Source/en.lproj/Localizable.strings

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,13 @@
180180
"Cannot add new phrases when Bopomofo annotation is on" = "Cannot add new phrases when Bopomofo annotation is on";
181181

182182
"Add Variant Annotation (Need BPMF IVS Font Support)" = "Add Variant Annotation (Need BPMF IVS Font Support)";
183+
184+
"Unicode Name" = "Unicode Name";
185+
"Phoenetic" = "Phoenetic";
186+
"Pinyin" = "Pinyin";
187+
"Canjie" = "Canjie";
188+
"Canjie Keys" = "Canjie Keys";
189+
"Japanese" = "Japanese";
190+
"Japanese Kun" = "Japanese Kun";
191+
"Japanese On" = "Japanese On";
192+
"Korean" = "Korean";

Source/zh-Hant.lproj/Localizable.strings

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,13 @@
178178
"Cannot add new phrases when Bopomofo annotation is on" = "注音字型破音字標記模式開啟時,不能增加新詞";
179179

180180
"Add Variant Annotation (Need BPMF IVS Font Support)" = "加上破音字標記 (需要使用注音字體)";
181+
182+
"Unicode Name" = "Unicode 名稱";
183+
"Phoenetic" = "注音";
184+
"Pinyin" = "拼音";
185+
"Canjie" = "倉頡字根";
186+
"Canjie Keys" = "倉頡按鍵";
187+
"Japanese" = "日文";
188+
"Japanese Kun" = "日文訓讀";
189+
"Japanese On" = "日文音讀";
190+
"Korean" = "韓文";

0 commit comments

Comments
 (0)