From 040c5973453fd30e218413014dceaa66369ae48f Mon Sep 17 00:00:00 2001 From: ShikiSuen Date: Mon, 26 Feb 2024 23:54:13 +0800 Subject: [PATCH] Shared // +CandidateTextService. --- .../CandidateTextService_SelectorImpl.swift | 148 ++++++++++++++++++ .../CandidateServiceCoordinatorTests.swift | 126 +++++++++++++++ .../Sources/Shared/CandidateTextService.swift | 102 ++++++++++++ .../Tests/SharedTests/SharedTests.swift | 17 ++ 4 files changed, 393 insertions(+) create mode 100644 Packages/vChewing_MainAssembly/Sources/MainAssembly/CandidateTextService_SelectorImpl.swift create mode 100644 Packages/vChewing_MainAssembly/Tests/MainAssemblyTests/CandidateServiceCoordinatorTests.swift create mode 100644 Packages/vChewing_Shared/Sources/Shared/CandidateTextService.swift diff --git a/Packages/vChewing_MainAssembly/Sources/MainAssembly/CandidateTextService_SelectorImpl.swift b/Packages/vChewing_MainAssembly/Sources/MainAssembly/CandidateTextService_SelectorImpl.swift new file mode 100644 index 00000000..4b420bbe --- /dev/null +++ b/Packages/vChewing_MainAssembly/Sources/MainAssembly/CandidateTextService_SelectorImpl.swift @@ -0,0 +1,148 @@ +// (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: - 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 : "\(subValue)(\(subKey))" + } + result = composed + } + + func prepareTextBookZhuyinReadings(_ param: CandidateTextService.CandidatePairServicable) { + let newReadings = param.reading.map { currentReading in + if currentReading.contains("_") { return "_??" } + return Tekkon.cnvPhonaToTextbookReading(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 + } +} diff --git a/Packages/vChewing_MainAssembly/Tests/MainAssemblyTests/CandidateServiceCoordinatorTests.swift b/Packages/vChewing_MainAssembly/Tests/MainAssemblyTests/CandidateServiceCoordinatorTests.swift new file mode 100644 index 00000000..cf639e13 --- /dev/null +++ b/Packages/vChewing_MainAssembly/Tests/MainAssemblyTests/CandidateServiceCoordinatorTests.swift @@ -0,0 +1,126 @@ +// (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 CocoaExtension +import InputMethodKit +import LangModelAssembly +@testable import MainAssembly +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_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 = "(ㄊㄧㄢˊ)(˙ㄉㄜ)" + 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 = "(tiě)(zuǐ)" + 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) + } + } +} diff --git a/Packages/vChewing_Shared/Sources/Shared/CandidateTextService.swift b/Packages/vChewing_Shared/Sources/Shared/CandidateTextService.swift new file mode 100644 index 00000000..b11f693a --- /dev/null +++ b/Packages/vChewing_Shared/Sources/Shared/CandidateTextService.swift @@ -0,0 +1,102 @@ +// (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 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 + } +} + +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) + } + } +} diff --git a/Packages/vChewing_Shared/Tests/SharedTests/SharedTests.swift b/Packages/vChewing_Shared/Tests/SharedTests/SharedTests.swift index 28ea972a..0c0c1018 100644 --- a/Packages/vChewing_Shared/Tests/SharedTests/SharedTests.swift +++ b/Packages/vChewing_Shared/Tests/SharedTests/SharedTests.swift @@ -10,9 +10,26 @@ 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) + } }