Shared // +CandidateTextService.
This commit is contained in:
parent
923471c8bb
commit
040c597345
|
@ -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 : "<ruby>\(subValue)<rp>(</rp><rt>\(subKey)</rt><rp>)</rp></ruby>"
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 = "<ruby>甜<rp>(</rp><rt>ㄊㄧㄢˊ</rt><rp>)</rp></ruby><ruby>的<rp>(</rp><rt>˙ㄉㄜ</rt><rp>)</rp></ruby>"
|
||||||
|
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 = "<ruby>鐵<rp>(</rp><rt>tiě</rt><rp>)</rp></ruby><ruby>嘴<rp>(</rp><rt>zuǐ</rt><rp>)</rp></ruby>"
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,9 +10,26 @@
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
final class SharedTests: XCTestCase {
|
final class SharedTests: XCTestCase {
|
||||||
|
// MARK: - PrefMgr().dumpShellScriptBackup()
|
||||||
|
|
||||||
func testDumpedPrefs() throws {
|
func testDumpedPrefs() throws {
|
||||||
let prefs = PrefMgr()
|
let prefs = PrefMgr()
|
||||||
let fetched = prefs.dumpShellScriptBackup() ?? ""
|
let fetched = prefs.dumpShellScriptBackup() ?? ""
|
||||||
XCTAssertFalse(fetched.isEmpty)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue