Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions McBopomofo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
6AFF97F2253B299E007F1C49 /* NonModalAlertWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6AFF97F0253B299E007F1C49 /* NonModalAlertWindowController.xib */; };
B058C5272AC9DF51002EDD66 /* ServiceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B058C5262AC9DF51002EDD66 /* ServiceProvider.swift */; };
B0781B352ACA2655003D9F75 /* ServicesMenu.strings in Resources */ = {isa = PBXBuildFile; fileRef = B0781B372ACA2655003D9F75 /* ServicesMenu.strings */; };
D40EFFA22EB669F900C1728A /* InfoCollector in Frameworks */ = {isa = PBXBuildFile; productRef = D40EFFA12EB669F900C1728A /* InfoCollector */; };
D41355D8278D74B5005E5CBD /* LanguageModelManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = D41355D7278D7409005E5CBD /* LanguageModelManager.mm */; };
D41355DB278E6D17005E5CBD /* McBopomofoLM.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D41355D9278E6D17005E5CBD /* McBopomofoLM.cpp */; };
D41355DE278EA3ED005E5CBD /* UserPhrasesLM.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D41355DC278EA3ED005E5CBD /* UserPhrasesLM.cpp */; };
Expand Down Expand Up @@ -177,6 +178,7 @@
B058C5262AC9DF51002EDD66 /* ServiceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceProvider.swift; sourceTree = "<group>"; };
B0781B362ACA2655003D9F75 /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/ServicesMenu.strings; sourceTree = "<group>"; };
B0781B382ACA2659003D9F75 /* zh-Hant */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/ServicesMenu.strings"; sourceTree = "<group>"; };
D40EFFA02EB669BB00C1728A /* InfoCollector */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = InfoCollector; path = Packages/InfoCollector; sourceTree = "<group>"; };
D41355D6278D7409005E5CBD /* LanguageModelManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LanguageModelManager.h; sourceTree = "<group>"; };
D41355D7278D7409005E5CBD /* LanguageModelManager.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = LanguageModelManager.mm; sourceTree = "<group>"; };
D41355D9278E6D17005E5CBD /* McBopomofoLM.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = McBopomofoLM.cpp; sourceTree = "<group>"; };
Expand Down Expand Up @@ -258,6 +260,7 @@
D48F853E2EACDB6C00C2FDAB /* RomanNumbers in Frameworks */,
D4E7917A2B52CDE500676A68 /* ChineseNumbers in Frameworks */,
D427F76A278C9E29004A2160 /* CandidateUI in Frameworks */,
D40EFFA22EB669F900C1728A /* InfoCollector in Frameworks */,
D4C9CAB127AAC9690058DFEA /* NSStringUtils in Frameworks */,
D427F7AE27907B8A004A2160 /* NotifierUI in Frameworks */,
D41B626C2B86EAE900583148 /* BopomofoBraille in Frameworks */,
Expand Down Expand Up @@ -459,6 +462,7 @@
D427F766278C9CBD004A2160 /* Packages */ = {
isa = PBXGroup;
children = (
D40EFFA02EB669BB00C1728A /* InfoCollector */,
D4E5EEE22E68A9B30068BC58 /* SystemCharacterInfo */,
D41B626A2B86EAD400583148 /* BopomofoBraille */,
D4E791782B52CDCF00676A68 /* ChineseNumbers */,
Expand Down Expand Up @@ -544,6 +548,7 @@
D41B626B2B86EAE900583148 /* BopomofoBraille */,
D4451AC82E688C6B00E8F5AB /* SystemCharacterInfo */,
D48F853D2EACDB6C00C2FDAB /* RomanNumbers */,
D40EFFA12EB669F900C1728A /* InfoCollector */,
);
productName = McBopomofo;
productReference = 6A0D4EA215FC0D2D00ABF4B3 /* McBopomofo.app */;
Expand Down Expand Up @@ -1436,6 +1441,10 @@
/* End XCConfigurationList section */

/* Begin XCSwiftPackageProductDependency section */
D40EFFA12EB669F900C1728A /* InfoCollector */ = {
isa = XCSwiftPackageProductDependency;
productName = InfoCollector;
};
D41B626B2B86EAE900583148 /* BopomofoBraille */ = {
isa = XCSwiftPackageProductDependency;
productName = BopomofoBraille;
Expand Down
74 changes: 66 additions & 8 deletions Packages/BopomofoBraille/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,75 @@

Copyright (c) 2022 and onwards The McBopomofo Authors.

The package includes a converter that translates Taiwanese Bopomofo into
Taiwanese Braille, and vice versa.
BopomofoBraille is a Swift package that converts between Taiwanese Bopomofo (注
音符號) and Taiwanese Braille (臺灣點字). It powers the Braille typing feature
in McBopomofo’s Service menu and can also be embedded in standalone projects
that need to translate between these two writing systems.

The main class in the package is BopomofoBrailleConverter. You can use its
methods directly. For example:
## Installation

Add the package dependency to your `Package.swift`:

```swift
dependencies: [
.package(path: "../Packages/BopomofoBraille"),
],
targets: [
.target(
name: "YourTarget",
dependencies: ["BopomofoBraille"]
),
]
```

When using the package outside of the McBopomofo repository, replace the local
path with the corresponding Git URL and version requirement.

## Usage

The primary entry point is `BopomofoBrailleConverter`. It exposes two static
conversion methods:

```swift
let bpmf = "ㄓㄨㄥㄨㄣˊㄓㄨˋㄧㄣ"
let convertedBraille = BopomofoBrailleConverter.convert(bopomofo: bpmf)
let convertedBpmf = BopomofoBrailleConverter.convert(braille: convertedBraille)
let braille = BopomofoBrailleConverter.convert(bopomofo: bpmf)

let roundTrip = BopomofoBrailleConverter.convert(braille: braille)
// roundTrip == bpmf
```

Both conversions preserve whitespace, punctuation, Latin letters, and digits
according to the Taiwanese Braille standard. Invalid syllables fall back to
spacing rules so that mixed-content strings remain legible.

### Working With Individual Syllables

`BopomofoSyllable` provides validation and direct access to the Braille for a
single syllable:

```swift
let syllable = try BopomofoSyllable(rawValue: "ㄉㄧˋ")
print(syllable.braille) // ⠙⠡⠐

let reversed = try BopomofoSyllable(braille: "⠋⠪⠂")
print(reversed.rawValue) // ㄊㄧㄠˊ
```

The package helps implement the features in the Service menu, allowing users to
input Taiwanese Braille by pressing the Ctrl and Enter keys.
Specialised token types such as `Letter`, `Digit`, and punctuation helpers are
exposed for callers that need finer control over parsing.

## Testing

Run the package test suite with:

```bash
swift test --package-path Packages/BopomofoBraille
```

The tests cover round-trip conversion, syllable validation, and edge cases for
Latin letters, digits, and punctuation.

## License

This package is released under the MIT license. See the header comments in the
source files for the full text.
152 changes: 152 additions & 0 deletions Packages/CandidateUI/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# CandidateUI

Copyright (c) 2022 and onwards The McBopomofo Authors.

CandidateUI provides the Cocoa candidate window implementation shared by the
McBopomofo input method targets. It bundles a base `CandidateController` with
ready-to-use horizontal and vertical variants that expose an Objective-C
compatible delegate API, making it easy to embed the UI in IMK and AppKit based
projects.

## Features

- Horizontal and vertical `NSWindowController` subclasses ready for IMK/AppKit
hosts
- Delegate driven data source with optional readings and explanations
- Configurable key labels, candidate fonts, tooltip text, and accessibility
notifications
- Built-in pagination and highlight navigation helpers for keyboard control
- Objective-C bridging annotations for seamless use from mixed Swift/ObjC code

## Requirements

- macOS 10.15 or later (AppKit based host process)
- Swift 5.9 or newer (Xcode 15.3 or newer)
- Interactions must occur on the main queue because the window is UI backed

## Installation

Add the package dependency to your `Package.swift`:

```swift
dependencies: [
.package(path: "../Packages/CandidateUI"),
],
targets: [
.target(
name: "YourTarget",
dependencies: ["CandidateUI"]
),
]
```

When integrating outside the McBopomofo repository, replace the local path with
the corresponding Git URL and version requirement.

## Usage

Choose either `HorizontalCandidateController` or `VerticalCandidateController`
depending on the layout you want.

### 1. Implement the delegate

Adopt `CandidateControllerDelegate` to expose candidates, optional readings,
and selection callbacks:

```swift
@MainActor
final class DemoDelegate: NSObject, CandidateControllerDelegate {
private let entries = ["ZhuYin", "Input Method", "McBopomofo"]
private let readings = ["zhu4", "shu1", "mai4"]

func candidateCountForController(_ controller: CandidateController) -> UInt {
UInt(entries.count)
}

func candidateController(_ controller: CandidateController, candidateAtIndex index: UInt) -> String {
entries[Int(index)]
}

func candidateController(_ controller: CandidateController, readingAtIndex index: UInt) -> String? {
readings[Int(index)]
}

func candidateController(
_ controller: CandidateController,
requestExplanationFor candidate: String,
reading: String
) -> String? {
nil
}

func candidateController(_ controller: CandidateController, didSelectCandidateAtIndex index: UInt) {
print("Selected candidate: \(entries[Int(index)])")
}
}
```

### 2. Configure the controller

Instantiate the controller on the main thread, assign the delegate, and adjust
appearance hints. Call `reloadData()` after mutating underlying candidate data.

```swift
let controller = VerticalCandidateController()
controller.delegate = DemoDelegate()
controller.keyLabels = ["1", "2", "3"].map { CandidateKeyLabel(key: $0, displayedText: $0) }
controller.keyLabelFont = NSFont.monospacedSystemFont(ofSize: 13, weight: .regular)
controller.candidateFont = NSFont.systemFont(ofSize: 18)
controller.tooltip = NSLocalizedString("Press arrow keys to browse candidates", comment: "Candidate window tooltip")
controller.set(windowTopLeftPoint: NSPoint(x: 320, y: 400), bottomOutOfScreenAdjustmentHeight: 24)
controller.visible = true
```

`set(windowTopLeftPoint:bottomOutOfScreenAdjustmentHeight:)` keeps the window
visible by nudging it back on screen when it would otherwise cross a screen
edge.

### 3. React to keyboard events

Use the navigation helpers to mirror the IMK key handling semantics:

```swift
@discardableResult
func handleCandidateNavigation(event: NSEvent, controller: CandidateController) -> Bool {
guard let specialKey = event.specialKey else { return false }
switch specialKey {
case .rightArrow: return controller.showNextPage()
case .leftArrow: return controller.showPreviousPage()
case .downArrow: return controller.highlightNextCandidate()
case .upArrow: return controller.highlightPreviousCandidate()
default: return false
}
}
```

For number keys, call `candidateIndexAtKeyLabelIndex(_:)` to translate the
pressed label into a candidate index (`UInt.max` signals “not available”). When
a new selection is confirmed, invoke the delegate’s
`candidateController(_:didSelectCandidateAtIndex:)` method to commit the choice
back to your input pipeline.

### Objective-C integration

All public controllers and protocols are annotated with `@objc`. Import the
generated umbrella header (for example through `McBopomofo-Bridging-Header.h`)
to drive the UI from Objective-C or Objective-C++ sources.

## Testing

Run the package tests with:

```bash
swift test --package-path Packages/CandidateUI
```

The test suite covers pagination, selection handling, and layout options for
both horizontal and vertical controllers.

## License

This package is released under the MIT license. The full text is available in
the source file headers.
101 changes: 101 additions & 0 deletions Packages/ChineseNumbers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# ChineseNumbers

Copyright (c) 2022 and onwards The McBopomofo Authors.

ChineseNumbers converts decimal numbers into Traditional Chinese numeral
strings. It ships with the McBopomofo input method project and exposes `@objc`
entry points so the same conversion logic works in Swift and Objective-C
codebases.

## Features

- Formats integer and decimal sections using lowercase (如「一二三」) or
uppercase (如「壹貳參」) digits via `ChineseNumbers.Case`
- Supports large magnitudes through `載` by chunking the input into four-digit
sections (萬、億、兆、京…)
- Emits the `點` separator for fractional values and preserves significant
trailing digits
- Provides the `SuzhouNumbers` helper for 蘇州碼 output, including unit labels
and vertical digit preferences
- Includes string utilities for trimming/padding zeros so callers can feed
pre-validated numeric text
- Fully accessible from Objective-C thanks to `@objc` annotations and `NSObject`
inheritance

## Installation

Add ChineseNumbers to the `dependencies` section of your `Package.swift`:

```swift
.dependencies([
.package(path: "Packages/ChineseNumbers")
])
```

Then link the library from a target:

```swift
.target(
name: "YourTarget",
dependencies: [
.product(name: "ChineseNumbers", package: "ChineseNumbers")
]
)
```

If you vend the package from another repository, replace the `.package` path
with the appropriate `.package(url: "…", from: "…")` declaration.

## Usage

```swift
import ChineseNumbers

let integerPart = "1234567890"
let decimalPart = "050"

let lowercase = ChineseNumbers.generate(
intPart: integerPart,
decPart: decimalPart,
digitCase: .lowercase
) // "一十二億三千四百五十六萬七千八百九十點〇五"

let uppercase = ChineseNumbers.generate(
intPart: integerPart,
decPart: decimalPart,
digitCase: .uppercase
) // "壹拾貳億參仟肆佰伍拾陸萬柒仟捌佰玖拾點零伍"

let suzhou = SuzhouNumbers.generate(
intPart: "123",
decPart: "40",
unit: "元",
preferInitialVertical: true
)
/*
〡二〣〤
百元
*/
```

`ChineseNumbers.generate` expects pre-separated integer and decimal strings so
you can source them from text fields without lossy floating-point conversion.
`SuzhouNumbers.generate` alternates between vertical digits (〡〢〣) and
horizontal strokes (一二三) based on the `preferInitialVertical` flag and
appends the appropriate place name when multiple characters are produced.

## Testing

Run the package tests with:

```bash
swift test
```

The XCTest suite covers zero trimming, lowercase/uppercase conversions,
fractional rendering, and Suzhou numeral generation.

## License

This package is released under the MIT license. The full text is available in
the source file headers.
Loading