126 lines
4.4 KiB
Swift
126 lines
4.4 KiB
Swift
import Foundation
|
|
|
|
public struct DictionaryEntry: Identifiable, Equatable, Sendable {
|
|
public enum Kind: Equatable, Sendable {
|
|
case term(String)
|
|
case correction(heard: String, wrote: String)
|
|
}
|
|
|
|
public let id: UUID
|
|
public var kind: Kind
|
|
|
|
public init(id: UUID = UUID(), kind: Kind) {
|
|
self.id = id
|
|
self.kind = kind
|
|
}
|
|
|
|
/// Equality ignores `id`: two entries with the same content are the same entry.
|
|
public static func == (lhs: DictionaryEntry, rhs: DictionaryEntry) -> Bool { lhs.kind == rhs.kind }
|
|
|
|
/// The text the correction pass listens for (a term listens for itself).
|
|
public var source: String {
|
|
switch kind {
|
|
case .term(let term): term
|
|
case .correction(let heard, _): heard
|
|
}
|
|
}
|
|
|
|
/// The text that ends up in the transcript.
|
|
public var wrote: String {
|
|
switch kind {
|
|
case .term(let term): term
|
|
case .correction(_, let wrote): wrote
|
|
}
|
|
}
|
|
}
|
|
|
|
public enum DictionaryLine: Equatable, Sendable {
|
|
case entry(DictionaryEntry)
|
|
case comment(String)
|
|
case blank
|
|
case invalid(String)
|
|
}
|
|
|
|
/// The dictionary as a list of lines, so comments, blank lines and order survive an edit.
|
|
public struct DictionaryFile: Equatable, Sendable {
|
|
public var lines: [DictionaryLine]
|
|
|
|
public init(lines: [DictionaryLine] = []) {
|
|
self.lines = lines
|
|
}
|
|
|
|
public init(parsing text: String) {
|
|
var raw = text.replacingOccurrences(of: "\r\n", with: "\n")
|
|
.split(separator: "\n", omittingEmptySubsequences: false)
|
|
.map(String.init)
|
|
if raw.last == "" { raw.removeLast() }
|
|
lines = raw.map(Self.parseLine)
|
|
}
|
|
|
|
static func parseLine(_ line: String) -> DictionaryLine {
|
|
let trimmed = line.trimmingCharacters(in: .whitespaces)
|
|
if trimmed.isEmpty { return .blank }
|
|
if trimmed.hasPrefix("#") { return .comment(line) }
|
|
if let arrow = trimmed.range(of: "=>") {
|
|
let heard = trimmed[..<arrow.lowerBound].trimmingCharacters(in: .whitespaces)
|
|
let wrote = trimmed[arrow.upperBound...].trimmingCharacters(in: .whitespaces)
|
|
guard !heard.isEmpty, !wrote.isEmpty else { return .invalid(line) }
|
|
return .entry(DictionaryEntry(kind: .correction(heard: heard, wrote: wrote)))
|
|
}
|
|
return .entry(DictionaryEntry(kind: .term(trimmed)))
|
|
}
|
|
|
|
/// Reuses the ids of `old` for entries with equal content (first unused match, in order), so ids survive a reload.
|
|
public func preservingIDs(from old: DictionaryFile) -> DictionaryFile {
|
|
var available = old.entries
|
|
var result = self
|
|
result.lines = lines.map { line in
|
|
guard case .entry(let entry) = line else { return line }
|
|
if let index = available.firstIndex(where: { $0.kind == entry.kind }) {
|
|
let reused = available.remove(at: index)
|
|
return .entry(DictionaryEntry(id: reused.id, kind: entry.kind))
|
|
}
|
|
return line
|
|
}
|
|
return result
|
|
}
|
|
|
|
public func serialized() -> String {
|
|
let out = lines.map { line -> String in
|
|
switch line {
|
|
case .entry(let entry):
|
|
switch entry.kind {
|
|
case .term(let term): return term
|
|
case .correction(let heard, let wrote): return "\(heard) => \(wrote)"
|
|
}
|
|
case .comment(let text), .invalid(let text): return text
|
|
case .blank: return ""
|
|
}
|
|
}
|
|
return out.isEmpty ? "" : out.joined(separator: "\n") + "\n"
|
|
}
|
|
|
|
public var entries: [DictionaryEntry] {
|
|
lines.compactMap { line in
|
|
if case .entry(let entry) = line { return entry }
|
|
return nil
|
|
}
|
|
}
|
|
|
|
@discardableResult
|
|
public mutating func append(_ kind: DictionaryEntry.Kind) -> UUID {
|
|
let entry = DictionaryEntry(kind: kind)
|
|
lines.append(.entry(entry))
|
|
return entry.id
|
|
}
|
|
|
|
public mutating func update(id: UUID, kind: DictionaryEntry.Kind) {
|
|
guard let index = lines.firstIndex(where: { if case .entry(let e) = $0 { return e.id == id } else { return false } }) else { return }
|
|
lines[index] = .entry(DictionaryEntry(id: id, kind: kind))
|
|
}
|
|
|
|
public mutating func remove(id: UUID) {
|
|
lines.removeAll { if case .entry(let e) = $0 { return e.id == id } else { return false } }
|
|
}
|
|
}
|