diff --git a/.swift-format b/.swift-format new file mode 100644 index 00000000..562386b8 --- /dev/null +++ b/.swift-format @@ -0,0 +1,11 @@ +{ + "version": 1, + "lineLength": 100, + "indentation": { + "spaces": 4 + }, + "maximumBlankLines": 1, + "respectsExistingLineBreaks": true, + "lineBreakBeforeControlFlowKeywords": false, + "lineBreakBeforeEachArgument": false +} diff --git a/McBopomofoTests/ServiceProviderTests.swift b/McBopomofoTests/ServiceProviderTests.swift index 3d42dd6f..626a6a6b 100644 --- a/McBopomofoTests/ServiceProviderTests.swift +++ b/McBopomofoTests/ServiceProviderTests.swift @@ -60,4 +60,15 @@ final class ServiceProviderTests: XCTestCase { let output = provider.extractReading(from: "!") XCTAssert(output == "_ctrl_punctuation_!", output) } + + func testConvertBrailleToChinese1() { + LanguageModelManager.loadDataModels() + let provider = ServiceProvider() + let helper = ServiceProviderInputHelper() + provider.delegate = helper as? any ServiceProviderDelegate + let input = "⠰⠤⠋⠺⠂⠻⠄⠛⠥⠂⠓⠫⠐⠑⠳⠄⠪⠐⠙⠮⠁⠅⠎⠐⠊⠱⠐⠑⠪⠄⠏⠣⠄⠇⠶⠐⠤⠆" + let expected = "「台灣人最需要的就是消波塊」" + let output = provider.convertBrailleToChineseText(string: input) + XCTAssert(output == expected, output) + } } diff --git a/Packages/BopomofoBraille/Sources/BopomofoBraille/BopomofoBraille.swift b/Packages/BopomofoBraille/Sources/BopomofoBraille/BopomofoBraille.swift index 842ee243..3fdfd68f 100644 --- a/Packages/BopomofoBraille/Sources/BopomofoBraille/BopomofoBraille.swift +++ b/Packages/BopomofoBraille/Sources/BopomofoBraille/BopomofoBraille.swift @@ -1,18 +1,21 @@ import Foundation -fileprivate protocol Syllable { +let kMinimalBopomofoLength = 1 +let kMinimalBrailleLength = 2 + +private protocol Syllable { var bopomofo: String { get } var braille: String { get } } -fileprivate protocol Combination { +private protocol Combination { var bopomofo: String { get } var braille: String { get } } // MARK: - Syllables -fileprivate enum Consonant: String, CaseIterable, Syllable { +private enum Consonant: String, CaseIterable, Syllable { fileprivate var bopomofo: String { return self.rawValue } @@ -66,7 +69,7 @@ fileprivate enum Consonant: String, CaseIterable, Syllable { fileprivate var isSingle: Bool { switch self { - case.ㄓ, .ㄔ, .ㄕ, .ㄖ, .ㄗ, .ㄘ, .ㄙ: + case .ㄓ, .ㄔ, .ㄕ, .ㄖ, .ㄗ, .ㄘ, .ㄙ: true default: false @@ -96,8 +99,7 @@ fileprivate enum Consonant: String, CaseIterable, Syllable { case ㄙ = "ㄙ" } - -fileprivate enum MiddleVowel: String, CaseIterable, Syllable { +private enum MiddleVowel: String, CaseIterable, Syllable { fileprivate var bopomofo: String { return self.rawValue } @@ -114,14 +116,17 @@ fileprivate enum MiddleVowel: String, CaseIterable, Syllable { } fileprivate func buildCombination(rawValue: String) throws -> Combination { - guard let result: Combination = switch self { - case .ㄧ: - ㄧ_Combination(rawValue: rawValue) - case .ㄨ: - ㄨ_Combination(rawValue: rawValue) - case .ㄩ: - ㄩ_Combination(rawValue: rawValue) - } else { + guard + let result: Combination = + switch self { + case .ㄧ: + ㄧ_Combination(rawValue: rawValue) + case .ㄨ: + ㄨ_Combination(rawValue: rawValue) + case .ㄩ: + ㄩ_Combination(rawValue: rawValue) + } + else { throw BopomofoSyllableError.invalidCharacter } return result @@ -132,12 +137,11 @@ fileprivate enum MiddleVowel: String, CaseIterable, Syllable { case ㄩ = "ㄩ" } - -fileprivate enum Vowel: String, CaseIterable, Syllable { +private enum Vowel: String, CaseIterable, Syllable { fileprivate var bopomofo: String { return self.rawValue } - + fileprivate var braille: String { switch self { case .ㄚ: @@ -186,7 +190,7 @@ fileprivate enum Vowel: String, CaseIterable, Syllable { // MARK: - Combination -fileprivate enum ㄧ_Combination: String, CaseIterable, Combination { +private enum ㄧ_Combination: String, CaseIterable, Combination { fileprivate var bopomofo: String { "ㄧ" + self.rawValue } @@ -228,7 +232,7 @@ fileprivate enum ㄧ_Combination: String, CaseIterable, Combination { case ㄧㄥ = "ㄥ" } -fileprivate enum ㄨ_Combination: String, CaseIterable, Combination { +private enum ㄨ_Combination: String, CaseIterable, Combination { fileprivate var bopomofo: String { "ㄨ" + self.rawValue } @@ -264,7 +268,7 @@ fileprivate enum ㄨ_Combination: String, CaseIterable, Combination { case ㄨㄥ = "ㄥ" } -fileprivate enum ㄩ_Combination: String, CaseIterable, Combination { +private enum ㄩ_Combination: String, CaseIterable, Combination { fileprivate var bopomofo: String { "ㄩ" + self.rawValue } @@ -291,7 +295,7 @@ fileprivate enum ㄩ_Combination: String, CaseIterable, Combination { // MARK: - Tone -fileprivate enum Tone: String, CaseIterable { +private enum Tone: String, CaseIterable { fileprivate var bopomofo: String { return self.rawValue } @@ -322,6 +326,7 @@ fileprivate enum Tone: String, CaseIterable { /// Errors for `BopomofoSyllable`. public enum BopomofoSyllableError: Error, LocalizedError { + case invalidLength case invalidCharacter case duplicatedConsonant case consonantShouldBeAtFront @@ -336,6 +341,8 @@ public enum BopomofoSyllableError: Error, LocalizedError { public var errorDescription: String? { switch self { + case .invalidLength: + "Invalid length" case .invalidCharacter: "Invalid character" case .duplicatedConsonant: @@ -365,12 +372,12 @@ public enum BopomofoSyllableError: Error, LocalizedError { /// Represents the Bopomofo syllables. public struct BopomofoSyllable { private static let consonantValues = Set(Consonant.allCases.map { $0.rawValue }) - private static let middleVowelValues = Set(MiddleVowel.allCases.map{ $0.rawValue }) + private static let middleVowelValues = Set(MiddleVowel.allCases.map { $0.rawValue }) private static let vowelValues = Set(Vowel.allCases.map { $0.rawValue }) private static let toneValues = Set(Tone.allCases.map { $0.rawValue }) private static let consonantBraille = Set(Consonant.allCases.map { $0.braille }) - private static let middleVowelBraille = Set(MiddleVowel.allCases.map{ $0.braille }) + private static let middleVowelBraille = Set(MiddleVowel.allCases.map { $0.braille }) private static let vowelBraille = Set(Vowel.allCases.map { $0.braille }) private static let toneBraille = Set(Tone.allCases.map { $0.braille }) private static let ㄧBraille = Set(ㄧ_Combination.allCases.map { $0.braille }) @@ -381,6 +388,10 @@ public struct BopomofoSyllable { public var braille: String public init(rawValue: String) throws { + if rawValue.count < kMinimalBopomofoLength { + throw BopomofoSyllableError.invalidLength + } + var consonant: Consonant? var middleVowel: MiddleVowel? var vowel: Vowel? @@ -410,7 +421,7 @@ public struct BopomofoSyllable { throw BopomofoSyllableError.vowelAlreadySet } if let middleVowel { - _ = try middleVowel.buildCombination(rawValue:s) + _ = try middleVowel.buildCombination(rawValue: s) } vowel = Vowel(rawValue: s) @@ -432,11 +443,15 @@ public struct BopomofoSyllable { } public init(braille: String) throws { + + if braille.count < kMinimalBrailleLength { + throw BopomofoSyllableError.invalidLength + } + func shouldConnectWithYiOrYv(_ next: String) -> Bool { - return next == MiddleVowel.ㄧ.braille || - next == MiddleVowel.ㄩ.braille || - BopomofoSyllable.ㄧBraille.contains(next) || - BopomofoSyllable.ㄩBraille.contains(next) + return next == MiddleVowel.ㄧ.braille || next == MiddleVowel.ㄩ.braille + || BopomofoSyllable.ㄧBraille.contains(next) + || BopomofoSyllable.ㄩBraille.contains(next) } var consonant: Consonant? @@ -455,7 +470,7 @@ public struct BopomofoSyllable { if let consonant, consonant.isSingle == false { throw BopomofoSyllableError.other } - case "⠁": // ㄓ or tone5 + case "⠁": // ㄓ or tone5 if index == 0 { consonant = Consonant.ㄓ } else { @@ -467,7 +482,7 @@ public struct BopomofoSyllable { } tone = .tone5 } - case "⠑": // ㄙ and ㄒ + case "⠑": // ㄙ and ㄒ if consonant != nil { throw BopomofoSyllableError.duplicatedConsonant } @@ -482,7 +497,7 @@ public struct BopomofoSyllable { } else { consonant = .ㄙ } - case "⠚": // ㄑ and ㄘ + case "⠚": // ㄑ and ㄘ if consonant != nil { throw BopomofoSyllableError.duplicatedConsonant } @@ -497,7 +512,7 @@ public struct BopomofoSyllable { } else { consonant = .ㄘ } - case "⠅": // ㄍ and ㄐ + case "⠅": // ㄍ and ㄐ if consonant != nil { throw BopomofoSyllableError.duplicatedConsonant } @@ -598,7 +613,7 @@ public struct BopomofoSyllable { throw BopomofoSyllableError.invalidCharacter } } - + guard let tone = tone else { throw BopomofoSyllableError.noTone } @@ -613,10 +628,8 @@ public struct BopomofoSyllable { _ vowel: Vowel?, _ tone: Tone ) -> String { - return (consonant?.rawValue ?? "") + - (middleVowel?.rawValue ?? "") + - (vowel?.rawValue ?? "") + - tone.rawValue + return (consonant?.rawValue ?? "") + (middleVowel?.rawValue ?? "") + (vowel?.rawValue ?? "") + + tone.rawValue } static private func makeBraille( diff --git a/Packages/BopomofoBraille/Sources/BopomofoBraille/Converter.swift b/Packages/BopomofoBraille/Sources/BopomofoBraille/Converter.swift index 46d1e592..9ff04a70 100644 --- a/Packages/BopomofoBraille/Sources/BopomofoBraille/Converter.swift +++ b/Packages/BopomofoBraille/Sources/BopomofoBraille/Converter.swift @@ -10,9 +10,9 @@ import Foundation let length = bopomofo.count while readHead < length { - let target = min(4, length - readHead) + let target = min(3, length - readHead - 1) var found = false - for i in (0.. 0 { + for i in (1...target).reversed() { + let start = braille.index(braille.startIndex, offsetBy: readHead) + let end = braille.index(braille.startIndex, offsetBy: readHead + i) + let substring = braille[start...end] + do { + let b = try BopomofoSyllable(braille: String(substring)) + output += b.rawValue + readHead += i + 1 + found = true + break + } catch { + // pass + } } } if !found { - for i in (0.. [Any] { var output: [Any] = [] @@ -109,34 +107,37 @@ import Foundation let length = braille.count while readHead < length { - let target = min(3, length - readHead) + var target = min(2, length - readHead - 1) var found = false - for i in (0.. 0 { + for i in (1...target).reversed() { + let start = braille.index(braille.startIndex, offsetBy: readHead) + let end = braille.index(braille.startIndex, offsetBy: readHead + i) + let substring = braille[start...end] + do { + let b = try BopomofoSyllable(braille: String(substring)) + readHead += i + 1 + if !text.isEmpty { + output.append(text) + text = "" + } + output.append(b) + found = true + break + } catch { + // pass } - found = true - break - } catch { - // pass } } if !found { - for i in (0.. String + func serviceProvider(didRequestCommitting provider: ServiceProvider) -> String + @objc(serviceProviderDidRequestReset:) + func serviceProvider(didRequestReset provider: ServiceProvider) } class ServiceProvider: NSObject { weak var delegate: ServiceProviderDelegate? - func extractReading(from firstWord:String) -> String { + func extractReading(from firstWord: String) -> String { var matches: [String] = [] // greedily find the longest possible matches @@ -50,7 +52,8 @@ class ServiceProvider: NSObject { while drop < substringCount { let candidate = String(substring.dropLast(drop)) if let converted = OpenCCBridge.shared.convertTraditional(candidate), - let match = LanguageModelManager.reading(for: converted) { + let match = LanguageModelManager.reading(for: converted) + { // append the match and skip over the matched portion matches.append(match) matchFrom = firstWord.index(matchFrom, offsetBy: substringCount - drop) @@ -72,7 +75,7 @@ class ServiceProvider: NSObject { @objc func addUserPhrase(_ pasteboard: NSPasteboard, userData: String?, error: NSErrorPointer) { guard let string = pasteboard.string(forType: .string), - let firstWord = string.components(separatedBy: .whitespacesAndNewlines).first + let firstWord = string.components(separatedBy: .whitespacesAndNewlines).first else { return } @@ -91,6 +94,8 @@ class ServiceProvider: NSObject { (NSApp.delegate as? AppDelegate)?.openUserPhrases(self) } + // MARK: - + @objc func addReading(_ pasteboard: NSPasteboard, userData: String?, error: NSErrorPointer) { guard let string = pasteboard.string(forType: .string) else { @@ -100,7 +105,8 @@ class ServiceProvider: NSObject { for c in string { output += String(c) if let converted = OpenCCBridge.shared.convertTraditional(String(c)), - let reading = LanguageModelManager.reading(for:converted) { + let reading = LanguageModelManager.reading(for: converted) + { if reading.isEmpty == false && reading.starts(with: "_") == false { output += "(\(reading))" } @@ -110,17 +116,15 @@ class ServiceProvider: NSObject { pasteboard.declareTypes([.string], owner: nil) pasteboard.writeObjects([output as NSString]) } +} - @objc func convertToReadings(_ pasteboard: NSPasteboard, userData: String?, error: NSErrorPointer) { - guard let string = pasteboard.string(forType: .string) - else { - return - } +extension ServiceProvider { + func convertToReadings(string: String) -> String { var output = "" for c in string { - if let converted = OpenCCBridge.shared.convertTraditional(String(c)), - let reading = LanguageModelManager.reading(for:converted) { + let reading = LanguageModelManager.reading(for: converted) + { if reading.isEmpty == false && reading.starts(with: "_") == false { output += reading } else { @@ -130,21 +134,28 @@ class ServiceProvider: NSObject { output += String(c) } } - pasteboard.clearContents() - pasteboard.declareTypes([.string], owner: nil) - pasteboard.writeObjects([output as NSString]) + return output } - @objc func convertToBraille(_ pasteboard: NSPasteboard, userData: String?, error: NSErrorPointer) { + @objc func convertToReadings( + _ pasteboard: NSPasteboard, userData: String?, error: NSErrorPointer + ) { guard let string = pasteboard.string(forType: .string) else { return } + var output = convertToReadings(string: string) + pasteboard.clearContents() + pasteboard.declareTypes([.string], owner: nil) + pasteboard.writeObjects([output as NSString]) + } + + func convertToBraille(string: String) -> String { var output = "" for c in string { - if let converted = OpenCCBridge.shared.convertTraditional(String(c)), - let reading = LanguageModelManager.reading(for:converted) { + let reading = LanguageModelManager.reading(for: converted) + { if reading.isEmpty == false && reading.starts(with: "_") == false { output += BopomofoBrailleConverter.convert(bopomofo: reading) } else { @@ -154,17 +165,25 @@ class ServiceProvider: NSObject { output += BopomofoBrailleConverter.convert(bopomofo: String(c)) } } - pasteboard.clearContents() - pasteboard.declareTypes([.string], owner: nil) - pasteboard.writeObjects([output as NSString]) + return output } - @objc func convertBrailleToChineseText(_ pasteboard: NSPasteboard, userData: String?, error: NSErrorPointer) { + @objc func convertToBraille( + _ pasteboard: NSPasteboard, userData: String?, error: NSErrorPointer + ) { guard let string = pasteboard.string(forType: .string) else { return } + var output = convertToBraille(string: string) + pasteboard.clearContents() + pasteboard.declareTypes([.string], owner: nil) + pasteboard.writeObjects([output as NSString]) + } + + func convertBrailleToChineseText(string: String) -> String { var output = "" + let test = BopomofoBrailleConverter.convert(braille: string) let tokens = BopomofoBrailleConverter.convert(brailleToTokens: string) for token in tokens { @@ -183,7 +202,17 @@ class ServiceProvider: NSObject { if let string = delegate?.serviceProvider(didRequestCommitting: self) { output += string } + return output + } + @objc func convertBrailleToChineseText( + _ pasteboard: NSPasteboard, userData: String?, error: NSErrorPointer + ) { + guard let string = pasteboard.string(forType: .string) + else { + return + } + var output = convertBrailleToChineseText(string: string) pasteboard.clearContents() pasteboard.declareTypes([.string], owner: nil) pasteboard.writeObjects([output as NSString]) diff --git a/Source/ServiceProviderInputHelper.mm b/Source/ServiceProviderInputHelper.mm index be219356..bd201c08 100644 --- a/Source/ServiceProviderInputHelper.mm +++ b/Source/ServiceProviderInputHelper.mm @@ -7,7 +7,6 @@ @interface ServiceProviderInputHelper() { std::shared_ptr _emptySharedPtr; Formosa::Gramambular2::ReadingGrid *_grid; - Formosa::Gramambular2::ReadingGrid::WalkResult _latestWalk; } @end @@ -27,7 +26,6 @@ - (instancetype)init if (self) { std::shared_ptr lm(_emptySharedPtr, [LanguageModelManager languageModelMcBopomofo]); _grid = new Formosa::Gramambular2::ReadingGrid(lm); - _latestWalk = Formosa::Gramambular2::ReadingGrid::WalkResult {}; } return self; } @@ -36,22 +34,32 @@ - (instancetype)init @implementation ServiceProviderInputHelper(ServiceProviderDelegate) +- (void)reset +{ + _grid->clear(); +} + - (void)serviceProvider:(ServiceProvider * _Nonnull)provider didRequestInsertReading:(NSString * _Nonnull)didRequestInsertReading { _grid->insertReading(didRequestInsertReading.UTF8String); - _latestWalk = _grid->walk(); } - (NSString * _Nonnull)serviceProviderDidRequestCommitting:(ServiceProvider * _Nonnull)provider { + Formosa::Gramambular2::ReadingGrid::WalkResult _latestWalk = _grid->walk(); std::string output; for (const auto& node : _latestWalk.nodes) { output += node->value(); } - _grid->clear(); - _latestWalk = Formosa::Gramambular2::ReadingGrid::WalkResult {}; + [self reset]; return [NSString stringWithUTF8String:output.c_str()]; } +- (void)serviceProviderDidRequestReset:(ServiceProvider * _Nonnull)provider +{ + [self reset]; +} + + @end