VCardParser.swift
173 lignes · 5758 octets
import Foundation /// vCard 3.0 / 4.0 import. enum VCardParser { static func parse(_ text: String) -> [ContactCard] { let unfolded = unfold(text) var cards: [ContactCard] = [] var buf: [String] = [] for line in unfolded.components(separatedBy: "\n") { let t = trimEnd(line) if equalsIgnoreCase(t, "BEGIN:VCARD") { buf = [] } else if equalsIgnoreCase(t, "END:VCARD") { cards.append(parseOne(buf)) } else if !t.isEmpty { buf.append(t) } } return cards } private static func equalsIgnoreCase(_ a: String, _ b: String) -> Bool { a.compare(b, options: .caseInsensitive) == .orderedSame } /// Kotlin `trimEnd()` — strips trailing whitespace. private static func trimEnd(_ s: String) -> String { var t = s while let last = t.last, last.isWhitespace { t.removeLast() } return t } private static func unfold(_ text: String) -> String { var out = "" let normalized = text .replacingOccurrences(of: "\r\n", with: "\n") .replacingOccurrences(of: "\r", with: "\n") for line in normalized.components(separatedBy: "\n") { if line.hasPrefix(" ") || line.hasPrefix("\t") { out += line.dropFirst() } else { if !out.isEmpty { out += "\n" } out += line } } return out } private static func parseOne(_ lines: [String]) -> ContactCard { var fullName: String? = nil var firstName: String? = nil var lastName: String? = nil var company: String? = nil var jobTitle: String? = nil var phones: [String] = [] var emails: [String] = [] var website: String? = nil var address: String? = nil var note: String? = nil for line in lines { guard let colon = line.firstIndex(of: ":") else { continue } let nameAndParams = String(line[..<colon]) let rawValue = String(line[line.index(after: colon)...]) let prop = propertyName(nameAndParams).uppercased() if prop == "PHOTO" || prop == "VERSION" || prop == "BEGIN" { continue } let value = unescape(rawValue) switch prop { case "FN": fullName = value.nilIfBlank case "N": let parts = splitUnescaped(rawValue) lastName = unescape(parts.count > 0 ? parts[0] : "").nilIfBlank firstName = unescape(parts.count > 1 ? parts[1] : "").nilIfBlank case "ORG": company = value.nilIfBlank case "TITLE": jobTitle = value.nilIfBlank case "TEL": if let v = value.nilIfBlank { phones.append(v) } case "EMAIL": if let v = value.nilIfBlank { emails.append(v) } case "URL": website = value.nilIfBlank case "ADR": let parts = splitUnescaped(rawValue) .map { unescape($0).trimmingCharacters(in: .whitespacesAndNewlines) } .filter { !$0.isEmpty } address = parts.joined(separator: ", ").nilIfBlank case "NOTE": note = value.nilIfBlank default: break } } return ContactCard( fullName: fullName, firstName: firstName, lastName: lastName, company: company, jobTitle: jobTitle, phones: phones, emails: emails, website: website, address: address, note: note ) } /// Property name before ':' : strip group prefix (item1.), take part before first ';'. private static func propertyName(_ nameAndParams: String) -> String { let afterGroup: String if let dot = nameAndParams.lastIndex(of: ".") { afterGroup = String(nameAndParams[nameAndParams.index(after: dot)...]) } else { afterGroup = nameAndParams } return String(afterGroup.prefix(while: { $0 != ";" })) } private static func unescape(_ value: String) -> String { var out = "" let chars = Array(value) var i = 0 while i < chars.count { let c = chars[i] if c == "\\" && i + 1 < chars.count { let n = chars[i + 1] switch n { case "n", "N": out.append("\n") case "r", "R": break // skip CR default: out.append(n) // covers ';' ',' '\\' and any other escaped char } i += 2 } else { out.append(c) i += 1 } } return out } /// Split on unescaped ';' (vCard structured values). private static func splitUnescaped(_ value: String) -> [String] { var parts: [String] = [] var buf = "" let chars = Array(value) var i = 0 while i < chars.count { let c = chars[i] if c == "\\" && i + 1 < chars.count { buf.append(c) buf.append(chars[i + 1]) i += 2 } else if c == ";" { parts.append(buf) buf = "" i += 1 } else { buf.append(c) i += 1 } } parts.append(buf) return parts } }
GitRust