From 7be2a85b25f1326ebdc12869027204c884d1411e Mon Sep 17 00:00:00 2001 From: ShikiSuen Date: Sat, 24 Feb 2024 20:40:14 +0800 Subject: [PATCH] BrailleSputnik // Initial Implementation. --- Packages/vChewing_BrailleSputnik/.gitignore | 8 + .../vChewing_BrailleSputnik/Package.swift | 35 +++ .../Sources/BrailleSputnik/BrailleEnums.swift | 284 ++++++++++++++++++ .../BrailleSputnik/BrailleSputnik.swift | 109 +++++++ .../BrailleSputnikTests.swift | 28 ++ Packages/vChewing_MainAssembly/Package.swift | 2 + vChewing.xcodeproj/project.pbxproj | 2 + 7 files changed, 468 insertions(+) create mode 100644 Packages/vChewing_BrailleSputnik/.gitignore create mode 100644 Packages/vChewing_BrailleSputnik/Package.swift create mode 100644 Packages/vChewing_BrailleSputnik/Sources/BrailleSputnik/BrailleEnums.swift create mode 100644 Packages/vChewing_BrailleSputnik/Sources/BrailleSputnik/BrailleSputnik.swift create mode 100644 Packages/vChewing_BrailleSputnik/Tests/BrailleSputnikTests/BrailleSputnikTests.swift diff --git a/Packages/vChewing_BrailleSputnik/.gitignore b/Packages/vChewing_BrailleSputnik/.gitignore new file mode 100644 index 00000000..0023a534 --- /dev/null +++ b/Packages/vChewing_BrailleSputnik/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Packages/vChewing_BrailleSputnik/Package.swift b/Packages/vChewing_BrailleSputnik/Package.swift new file mode 100644 index 00000000..e6251151 --- /dev/null +++ b/Packages/vChewing_BrailleSputnik/Package.swift @@ -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"] + ), + ] +) diff --git a/Packages/vChewing_BrailleSputnik/Sources/BrailleSputnik/BrailleEnums.swift b/Packages/vChewing_BrailleSputnik/Sources/BrailleSputnik/BrailleEnums.swift new file mode 100644 index 00000000..69a867bf --- /dev/null +++ b/Packages/vChewing_BrailleSputnik/Sources/BrailleSputnik/BrailleEnums.swift @@ -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 國通標準並未定義花括弧。 + ] + } +} diff --git a/Packages/vChewing_BrailleSputnik/Sources/BrailleSputnik/BrailleSputnik.swift b/Packages/vChewing_BrailleSputnik/Sources/BrailleSputnik/BrailleSputnik.swift new file mode 100644 index 00000000..74a0e406 --- /dev/null +++ b/Packages/vChewing_BrailleSputnik/Sources/BrailleSputnik/BrailleSputnik.swift @@ -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 + } +} diff --git a/Packages/vChewing_BrailleSputnik/Tests/BrailleSputnikTests/BrailleSputnikTests.swift b/Packages/vChewing_BrailleSputnik/Tests/BrailleSputnikTests/BrailleSputnikTests.swift new file mode 100644 index 00000000..f001c4b5 --- /dev/null +++ b/Packages/vChewing_BrailleSputnik/Tests/BrailleSputnikTests/BrailleSputnikTests.swift @@ -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, "⠙⠔⠆⠅⠳⠁⠅⠳⠁⠃⠡⠆⠇⠢⠗⠆⠅⠳⠁⠅⠳⠁⠅⠜⠂⠐⠎⠧⠁⠅⠳⠁⠅⠳⠁⠉⠪⠄⠜⠆⠎⠆⠅⠳⠁⠅⠳⠁⠖⠂⠐⠆") + } +} diff --git a/Packages/vChewing_MainAssembly/Package.swift b/Packages/vChewing_MainAssembly/Package.swift index 4e88ec2a..a10dde34 100644 --- a/Packages/vChewing_MainAssembly/Package.swift +++ b/Packages/vChewing_MainAssembly/Package.swift @@ -17,6 +17,7 @@ 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_Hotenka"), @@ -38,6 +39,7 @@ 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"), diff --git a/vChewing.xcodeproj/project.pbxproj b/vChewing.xcodeproj/project.pbxproj index 084c400f..e39bf78c 100644 --- a/vChewing.xcodeproj/project.pbxproj +++ b/vChewing.xcodeproj/project.pbxproj @@ -119,6 +119,7 @@ 5B40113A28D71B8700A9D4CB /* vChewing_Uninstaller */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = vChewing_Uninstaller; path = Packages/vChewing_Uninstaller; sourceTree = ""; }; 5B65B919284D0185007C558B /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 5B65FB322A9518C9007EEFB0 /* vChewing_MainAssembly */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = vChewing_MainAssembly; path = Packages/vChewing_MainAssembly; sourceTree = ""; }; + 5B6CA6272B8A1C9200A85050 /* vChewing_BrailleSputnik */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = vChewing_BrailleSputnik; path = Packages/vChewing_BrailleSputnik; sourceTree = ""; }; 5B70F4E52A0BE900005EA8C4 /* MenuIcon-TCVIM.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "MenuIcon-TCVIM.png"; sourceTree = ""; }; 5B70F4E62A0BE900005EA8C4 /* MenuIcon-SCVIM.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "MenuIcon-SCVIM.png"; sourceTree = ""; }; 5B70F4E72A0BE900005EA8C4 /* MenuIcon-SCVIM@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "MenuIcon-SCVIM@2x.png"; sourceTree = ""; }; @@ -340,6 +341,7 @@ 5BDB7A3028D47587001AC277 /* Jad_BookmarkManager */, 5BDB7A3428D47587001AC277 /* Qwertyyb_ShiftKeyUpChecker */, 5BDB7A3528D47587001AC277 /* RMJay_LineReader */, + 5B6CA6272B8A1C9200A85050 /* vChewing_BrailleSputnik */, 5BA8C30128DEFE4F004C5CC4 /* vChewing_CandidateWindow */, 5B963C9B28D5BE4100DCEE88 /* vChewing_CocoaExtension */, 5BDB7A3228D47587001AC277 /* vChewing_Hotenka */,