VCardParser.kt
135 lignes · 5077 octets
package fr.ebii.card2vcf.contact object VCardParser { fun parse(text: String): List<ContactCard> { val unfolded = unfold(text) val cards = mutableListOf<ContactCard>() var buf = mutableListOf<String>() for (line in unfolded.lineSequence()) { val t = line.trimEnd() when { t.equals("BEGIN:VCARD", true) -> buf = mutableListOf() t.equals("END:VCARD", true) -> cards.add(parseOne(buf)) else -> if (t.isNotEmpty()) buf.add(t) } } return cards } private fun unfold(text: String): String = buildString { for (line in text.replace("\r\n", "\n").replace('\r', '\n').lineSequence()) { if (line.startsWith(" ") || line.startsWith("\t")) { append(line.drop(1)) } else { if (isNotEmpty()) append('\n') append(line) } } } private fun parseOne(lines: List<String>): ContactCard { var fullName: String? = null var firstName: String? = null var lastName: String? = null var company: String? = null var jobTitle: String? = null val phones = mutableListOf<String>() val emails = mutableListOf<String>() var website: String? = null var address: String? = null var note: String? = null for (line in lines) { val colon = line.indexOf(':') if (colon < 0) continue val nameAndParams = line.substring(0, colon) val rawValue = line.substring(colon + 1) val prop = propertyName(nameAndParams) if (prop.equals("PHOTO", true) || prop.equals("VERSION", true) || prop.equals("BEGIN", true)) { continue } val value = unescape(rawValue) when { prop.equals("FN", true) -> fullName = value.takeIf { it.isNotBlank() } prop.equals("N", true) -> { val parts = splitUnescaped(rawValue) lastName = unescape(parts.getOrElse(0) { "" }).takeIf { it.isNotBlank() } firstName = unescape(parts.getOrElse(1) { "" }).takeIf { it.isNotBlank() } } prop.equals("ORG", true) -> company = value.takeIf { it.isNotBlank() } prop.equals("TITLE", true) -> jobTitle = value.takeIf { it.isNotBlank() } prop.equals("TEL", true) -> value.takeIf { it.isNotBlank() }?.let { phones.add(it) } prop.equals("EMAIL", true) -> value.takeIf { it.isNotBlank() }?.let { emails.add(it) } prop.equals("URL", true) -> website = value.takeIf { it.isNotBlank() } prop.equals("ADR", true) -> { val parts = splitUnescaped(rawValue) .map { unescape(it).trim() } .filter { it.isNotEmpty() } address = parts.joinToString(", ").takeIf { it.isNotBlank() } } prop.equals("NOTE", true) -> note = value.takeIf { it.isNotBlank() } } } 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 fun propertyName(nameAndParams: String): String { val afterGroup = nameAndParams.substringAfterLast('.', nameAndParams) return afterGroup.substringBefore(';') } private fun unescape(value: String): String = buildString(value.length) { var i = 0 while (i < value.length) { val c = value[i] if (c == '\\' && i + 1 < value.length) { when (val n = value[i + 1]) { 'n', 'N' -> append('\n') 'r', 'R' -> { /* skip CR */ } ';', ',', '\\' -> append(n) else -> append(n) } i += 2 } else { append(c) i++ } } } /** Split on unescaped ';' (vCard structured values). */ private fun splitUnescaped(value: String): List<String> { val parts = mutableListOf<String>() val buf = StringBuilder() var i = 0 while (i < value.length) { val c = value[i] if (c == '\\' && i + 1 < value.length) { buf.append(c).append(value[i + 1]) i += 2 } else if (c == ';') { parts.add(buf.toString()) buf.clear() i++ } else { buf.append(c) i++ } } parts.add(buf.toString()) return parts } }
GitRust