import: a label stays beside a line that will not read
Vcf.read keeps a line that will not read, but orphaned_groups skipped
it, so a group whose only other line was unreadable looked empty and
its label was dropped. Such a line now anchors its group, read off
its bytes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VAiyEheRBdzbBjZUDWZUPk
diff --git a/lib/pro_tacts/import/vcf.rb b/lib/pro_tacts/import/vcf.rb
index cc98d8a..db1faf4 100644
--- a/lib/pro_tacts/import/vcf.rb
+++ b/lib/pro_tacts/import/vcf.rb
@@ -185,20 +185,20 @@ module ProTacts
# without its `item3.X-ABRELATEDNAMES` would leave a name for a
# line that is not there. A grouped property this book reads —
# `item1.ADR`, say — anchors its own group, because it is coming
- # in.
+ # in, and so does a line that will not read, which comes in
+ # whatever it is (#read): its group is read off its bytes, there
+ # being no property to read it from.
#: (Array[VCard::Parser::Line] lines) -> Array[String]
def self.orphaned_groups(lines)
anchored = {} #: Hash[String, bool]
lines.each do |line|
property = line.property
- next if property.nil?
-
- group = property.group
+ group = property ? property.group : VCard::Parser.unfold(line.verbatim)[/\A([A-Za-z0-9-]+)\./, 1]
next if group.nil?
# A label is in its group without anchoring it, so a group
# holding nothing else is one with nothing but a label in it.
- anchors = !property.name.casecmp?(LABEL) && known?(property.name)
+ anchors = property.nil? || (!property.name.casecmp?(LABEL) && known?(property.name))
# Without case, as a property name compares and as
# EditedContact#label_of and Merge pair a label with its
# line: `ITEM1.X-ABLabel` names `item1.TEL`'s group.
diff --git a/test/pro_tacts/import/test_vcf.rb b/test/pro_tacts/import/test_vcf.rb
index ca1c1f9..1bdc33d 100644
--- a/test/pro_tacts/import/test_vcf.rb
+++ b/test/pro_tacts/import/test_vcf.rb
@@ -301,6 +301,7 @@ class ImportVcfTest < Minitest::Test
"item1.X-ABLabel:_$!<Mobile>!$_", "item2.X-ABRELATEDNAMES:Sam", "item2.X-ABLabel:_$!<Spouse>!$_",
"item3.X-ABLabel:orphan", "item4.ADR:;;1 Main St;;;;", "item4.X-ABRELATEDNAMES:Pat",
"item4.X-ABLabel:_$!<Home>!$_", "X-SOCIALPROFILE:x", "PRODID:-//Apple", "CATEGORIES:Family",
+ "item5.TEL;=:+1 555 0102", "item5.X-ABLabel:unreadable",
].freeze #: Array[String]
# Each line's group spelt in either case, as a client may write it
@@ -339,10 +340,15 @@ class ImportVcfTest < Minitest::Test
end
# The groups a reading's card keeps a label in, and the groups it
- # keeps anything else in.
- def labeled_groups(card) = card.properties.select { it.name.casecmp?(Vcf::LABEL) }.map { it.group&.downcase }.uniq
+ # keeps anything else in — a line that will not read included, its
+ # group read off its bytes.
+ def group_of(line) = (line.property&.group || line.verbatim[/\A([A-Za-z0-9-]+)\./, 1])&.downcase
- def anchored_groups(card) = card.properties.reject { it.name.casecmp?(Vcf::LABEL) }.filter_map { it.group&.downcase }.uniq
+ def label?(line) = line.property&.name&.casecmp?(Vcf::LABEL)
+
+ def labeled_groups(card) = card.lines.select { label?(it) }.map { group_of(it) }.uniq
+
+ def anchored_groups(card) = card.lines.reject { label?(it) }.filter_map { group_of(it) }.uniq
# A label names the line beside it, so none comes in without one.
def test_no_label_comes_in_without_a_line_to_name