@treeviz/familysearch-sdk 2.0.6 → 2.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -44,6 +44,64 @@ function transformSourceUrl(url) {
44
44
  }
45
45
  return `${baseUrl}/${arkId}`;
46
46
  }
47
+ function sanitizeGedcomValue(value) {
48
+ return value.replace(/[\r\n\t]+/g, " ").trim();
49
+ }
50
+ var GEDCOM_MONTHS = [
51
+ "JAN",
52
+ "FEB",
53
+ "MAR",
54
+ "APR",
55
+ "MAY",
56
+ "JUN",
57
+ "JUL",
58
+ "AUG",
59
+ "SEP",
60
+ "OCT",
61
+ "NOV",
62
+ "DEC"
63
+ ];
64
+ function formatSimpleFormalDate(value) {
65
+ const match = value.match(/^([+-])(\d{1,6})(?:-(\d{2}))?(?:-(\d{2}))?/);
66
+ if (!match) return void 0;
67
+ const [, sign, yearPart, monthPart, dayPart] = match;
68
+ if (sign === "-") return void 0;
69
+ const year = String(parseInt(yearPart, 10));
70
+ if (!monthPart) return year;
71
+ const month = GEDCOM_MONTHS[parseInt(monthPart, 10) - 1];
72
+ if (!month) return void 0;
73
+ if (!dayPart) return `${month} ${year}`;
74
+ return `${parseInt(dayPart, 10)} ${month} ${year}`;
75
+ }
76
+ function convertFormalDateToGedcom(formal) {
77
+ if (!formal) return void 0;
78
+ let value = formal.trim();
79
+ let approximate = false;
80
+ if (value.startsWith("A")) {
81
+ approximate = true;
82
+ value = value.slice(1);
83
+ }
84
+ if (value.includes("/")) {
85
+ const slashIndex = value.indexOf("/");
86
+ const start = value.slice(0, slashIndex);
87
+ const end = value.slice(slashIndex + 1);
88
+ const startDate = start ? formatSimpleFormalDate(start) : void 0;
89
+ const endDate = end ? formatSimpleFormalDate(end) : void 0;
90
+ if (start && !startDate) return void 0;
91
+ if (end && !endDate) return void 0;
92
+ if (startDate && endDate) return `BET ${startDate} AND ${endDate}`;
93
+ if (startDate) return `FROM ${startDate}`;
94
+ if (endDate) return `TO ${endDate}`;
95
+ return void 0;
96
+ }
97
+ const simple = formatSimpleFormalDate(value);
98
+ if (!simple) return void 0;
99
+ return approximate ? `ABT ${simple}` : simple;
100
+ }
101
+ function resolveFactDate(date) {
102
+ if (!date) return void 0;
103
+ return convertFormalDateToGedcom(date.formal) || date.original;
104
+ }
47
105
  function extractName(person) {
48
106
  if (!person) return null;
49
107
  if (person.names?.[0]?.nameForms?.[0]) {
@@ -51,8 +109,14 @@ function extractName(person) {
51
109
  const parts = nameForm.parts || [];
52
110
  const given = parts.find((p) => p.type?.includes("Given"))?.value || "";
53
111
  const surname = parts.find((p) => p.type?.includes("Surname"))?.value || "";
54
- if (given || surname) {
55
- return `${given} /${surname}/`.trim();
112
+ if (given && surname) {
113
+ return `${given} /${surname}/`;
114
+ }
115
+ if (surname) {
116
+ return `/${surname}/`;
117
+ }
118
+ if (given) {
119
+ return given;
56
120
  }
57
121
  if (nameForm.fullText) {
58
122
  return nameForm.fullText;
@@ -82,11 +146,9 @@ function extractFact(person, factType) {
82
146
  const factTypeUrl = `http://gedcomx.org/${factType === "BIRTH" ? "Birth" : "Death"}`;
83
147
  const fact = person.facts?.find((f) => f.type === factTypeUrl);
84
148
  if (fact) {
85
- if (fact.date?.formal || fact.date?.original) {
86
- result.date = (fact.date?.formal || fact.date?.original)?.replace(
87
- /^(-|\+)/,
88
- ""
89
- );
149
+ const resolvedDate = resolveFactDate(fact.date);
150
+ if (resolvedDate) {
151
+ result.date = resolvedDate;
90
152
  }
91
153
  if (fact.place?.original) {
92
154
  result.place = fact.place.original;
@@ -139,25 +201,30 @@ function convertFactToGedcom(fact) {
139
201
  };
140
202
  const gedcomTag = typeMap[fact.type] || "EVEN";
141
203
  if (gedcomTag === "OCCU" && fact.value) {
142
- lines.push(`1 OCCU ${fact.value}`);
204
+ lines.push(`1 OCCU ${sanitizeGedcomValue(fact.value)}`);
143
205
  } else {
144
206
  lines.push(`1 ${gedcomTag}`);
145
207
  }
208
+ if (gedcomTag === "EVEN") {
209
+ const typeName = fact.type.split("/").pop();
210
+ if (typeName) {
211
+ lines.push(`2 TYPE ${sanitizeGedcomValue(typeName)}`);
212
+ }
213
+ }
146
214
  if (fact.links?.conclusion?.href) {
147
215
  const webUrl = transformFamilySearchUrl(fact.links.conclusion.href);
148
216
  lines.push(`2 WWW ${webUrl}`);
149
217
  lines.push(`3 _IS_FS Y`);
150
218
  }
151
- if (fact.date?.formal || fact.date?.original) {
152
- lines.push(
153
- `2 DATE ${(fact.date?.formal || fact.date?.original)?.replace(/^(-|\+)/, "")}`
154
- );
219
+ const resolvedDate = resolveFactDate(fact.date);
220
+ if (resolvedDate) {
221
+ lines.push(`2 DATE ${sanitizeGedcomValue(resolvedDate)}`);
155
222
  }
156
223
  if (fact.place?.original) {
157
- lines.push(`2 PLAC ${fact.place.original}`);
224
+ lines.push(`2 PLAC ${sanitizeGedcomValue(fact.place.original)}`);
158
225
  }
159
226
  if (fact.value && gedcomTag !== "OCCU") {
160
- lines.push(`2 NOTE ${fact.value}`);
227
+ lines.push(`2 NOTE ${sanitizeGedcomValue(fact.value)}`);
161
228
  }
162
229
  return lines;
163
230
  }
@@ -177,14 +244,14 @@ function convertToGedcom(pedigreeData, options = {}) {
177
244
  lines.push("2 VERS 1.0");
178
245
  lines.push("2 NAME FamilySearch API");
179
246
  if (treeName) {
180
- lines.push(`2 _TREE ${treeName}`);
247
+ lines.push(`2 _TREE ${sanitizeGedcomValue(treeName)}`);
181
248
  const randomRIN = Math.floor(Math.random() * 1e8);
182
249
  lines.push(`3 RIN ${randomRIN}`);
183
250
  }
184
251
  lines.push("1 DEST ANY");
185
252
  lines.push("1 DATE " + formatDateForGedcom(/* @__PURE__ */ new Date()));
186
253
  lines.push("1 SUBM @SUBM1@");
187
- lines.push("1 FILE " + treeName);
254
+ lines.push("1 FILE " + sanitizeGedcomValue(treeName));
188
255
  lines.push("1 GEDC");
189
256
  lines.push("2 VERS 5.5");
190
257
  lines.push("2 FORM LINEAGE-LINKED");
@@ -206,20 +273,22 @@ function convertToGedcom(pedigreeData, options = {}) {
206
273
  const sourceId = `@S${sourceCounter++}@`;
207
274
  lines.push(`0 ${sourceId} SOUR`);
208
275
  const title = source.titles?.[0]?.value || "FamilySearch Source";
209
- lines.push(`1 TITL ${title}`);
276
+ lines.push(`1 TITL ${sanitizeGedcomValue(title)}`);
210
277
  if (source.citations?.[0]?.value) {
211
- lines.push(`1 TEXT ${source.citations[0].value}`);
278
+ lines.push(`1 TEXT ${sanitizeGedcomValue(source.citations[0].value)}`);
212
279
  }
213
280
  if (source.about) {
214
281
  const webUrl = transformSourceUrl(source.about);
215
282
  lines.push(`1 WWW ${webUrl}`);
216
283
  }
217
284
  if (source.resourceType) {
218
- lines.push(`1 NOTE Resource Type: ${source.resourceType}`);
285
+ lines.push(
286
+ `1 NOTE Resource Type: ${sanitizeGedcomValue(source.resourceType)}`
287
+ );
219
288
  }
220
289
  });
221
290
  const fsSourMap = /* @__PURE__ */ new Map();
222
- let fsSourCounter = 1;
291
+ let fsSourCounter = sourceCounter;
223
292
  pedigreeData.persons.forEach((person) => {
224
293
  if (person.sources?.persons?.[0]?.sources) {
225
294
  const sourceDescMap = /* @__PURE__ */ new Map();
@@ -269,20 +338,22 @@ function convertToGedcom(pedigreeData, options = {}) {
269
338
  lines.push(`1 _IS_FS Y`);
270
339
  lines.push(`1 _FS_ID ${id}`);
271
340
  if (source.titles?.[0]?.value) {
272
- lines.push(`1 TITL ${source.titles[0].value}`);
341
+ lines.push(`1 TITL ${sanitizeGedcomValue(source.titles[0].value)}`);
273
342
  }
274
343
  if (source.citations?.[0]?.value) {
275
- lines.push(`1 TEXT ${source.citations[0].value}`);
344
+ lines.push(`1 TEXT ${sanitizeGedcomValue(source.citations[0].value)}`);
276
345
  }
277
346
  if (source.about) {
278
347
  const webUrl = transformSourceUrl(source.about);
279
348
  lines.push(`1 WWW ${webUrl}`);
280
349
  }
281
350
  if (source.resourceType) {
282
- lines.push(`1 NOTE Resource Type: ${source.resourceType}`);
351
+ lines.push(
352
+ `1 NOTE Resource Type: ${sanitizeGedcomValue(source.resourceType)}`
353
+ );
283
354
  }
284
355
  notes.forEach((note) => {
285
- lines.push(`1 NOTE ${note}`);
356
+ lines.push(`1 NOTE ${sanitizeGedcomValue(note)}`);
286
357
  });
287
358
  });
288
359
  const fsMatchMap = /* @__PURE__ */ new Map();
@@ -302,7 +373,7 @@ function convertToGedcom(pedigreeData, options = {}) {
302
373
  lines.push(`1 _IS_FS Y`);
303
374
  lines.push(`1 _FS_ID ${id}`);
304
375
  if (entry.title) {
305
- lines.push(`1 TITL ${entry.title}`);
376
+ lines.push(`1 TITL ${sanitizeGedcomValue(entry.title)}`);
306
377
  }
307
378
  if (entry.content?.score !== void 0) {
308
379
  lines.push(`1 SCORE ${entry.content.score}`);
@@ -323,7 +394,7 @@ function convertToGedcom(pedigreeData, options = {}) {
323
394
  }
324
395
  if (matchInfo.status) {
325
396
  const statusName = matchInfo.status.includes("/") ? matchInfo.status.split("/").pop() : matchInfo.status;
326
- lines.push(`1 NOTE Status: ${statusName}`);
397
+ lines.push(`1 NOTE Status: ${sanitizeGedcomValue(statusName || "")}`);
327
398
  }
328
399
  }
329
400
  lines.push(`1 TYPE ${collectionType}`);
@@ -335,42 +406,52 @@ function convertToGedcom(pedigreeData, options = {}) {
335
406
  if (entry.content?.gedcomx?.persons && entry.content.gedcomx.persons.length > 0) {
336
407
  const matchedPerson = entry.content.gedcomx.persons[0];
337
408
  if (matchedPerson.display?.name) {
338
- lines.push(`1 TEXT ${matchedPerson.display.name}`);
409
+ lines.push(`1 TEXT ${sanitizeGedcomValue(matchedPerson.display.name)}`);
339
410
  }
340
411
  if (matchedPerson.display?.lifespan) {
341
412
  lines.push(
342
- `1 NOTE Lifespan: ${matchedPerson.display.lifespan}`
413
+ `1 NOTE Lifespan: ${sanitizeGedcomValue(matchedPerson.display.lifespan)}`
343
414
  );
344
415
  }
345
416
  if (matchedPerson.display?.birthDate || matchedPerson.display?.birthPlace) {
346
- const birthInfo = [
347
- matchedPerson.display.birthDate,
348
- matchedPerson.display.birthPlace
349
- ].filter(Boolean).join(", ");
417
+ const birthInfo = sanitizeGedcomValue(
418
+ [
419
+ matchedPerson.display.birthDate,
420
+ matchedPerson.display.birthPlace
421
+ ].filter(Boolean).join(", ")
422
+ );
350
423
  if (birthInfo) {
351
424
  lines.push(`1 NOTE Birth: ${birthInfo}`);
352
425
  }
353
426
  }
354
427
  if (matchedPerson.display?.deathDate || matchedPerson.display?.deathPlace) {
355
- const deathInfo = [
356
- matchedPerson.display.deathDate,
357
- matchedPerson.display.deathPlace
358
- ].filter(Boolean).join(", ");
428
+ const deathInfo = sanitizeGedcomValue(
429
+ [
430
+ matchedPerson.display.deathDate,
431
+ matchedPerson.display.deathPlace
432
+ ].filter(Boolean).join(", ")
433
+ );
359
434
  if (deathInfo) {
360
435
  lines.push(`1 NOTE Death: ${deathInfo}`);
361
436
  }
362
437
  }
363
438
  if (matchedPerson.display?.gender) {
364
- lines.push(`1 NOTE Gender: ${matchedPerson.display.gender}`);
439
+ lines.push(
440
+ `1 NOTE Gender: ${sanitizeGedcomValue(matchedPerson.display.gender)}`
441
+ );
365
442
  }
366
443
  }
367
444
  if (entry.content?.sourceDescription) {
368
445
  const sourceDesc = entry.content.sourceDescription;
369
446
  if (sourceDesc.citations?.[0]?.value) {
370
- lines.push(`1 NOTE Citation: ${sourceDesc.citations[0].value}`);
447
+ lines.push(
448
+ `1 NOTE Citation: ${sanitizeGedcomValue(sourceDesc.citations[0].value)}`
449
+ );
371
450
  }
372
451
  if (sourceDesc.resourceType) {
373
- lines.push(`1 NOTE Resource Type: ${sourceDesc.resourceType}`);
452
+ lines.push(
453
+ `1 NOTE Resource Type: ${sanitizeGedcomValue(sourceDesc.resourceType)}`
454
+ );
374
455
  }
375
456
  if (sourceDesc.about) {
376
457
  const webUrl = transformSourceUrl(sourceDesc.about);
@@ -613,7 +694,7 @@ function convertToGedcom(pedigreeData, options = {}) {
613
694
  const personData = person.fullDetails?.persons?.[0] || person;
614
695
  const name = extractName(personData);
615
696
  if (name) {
616
- lines.push(`1 NAME ${name}`);
697
+ lines.push(`1 NAME ${sanitizeGedcomValue(name)}`);
617
698
  }
618
699
  const gender = extractGender(personData);
619
700
  if (gender) {
@@ -623,20 +704,20 @@ function convertToGedcom(pedigreeData, options = {}) {
623
704
  if (birth) {
624
705
  lines.push("1 BIRT");
625
706
  if (birth.date) {
626
- lines.push(`2 DATE ${birth.date}`);
707
+ lines.push(`2 DATE ${sanitizeGedcomValue(birth.date)}`);
627
708
  }
628
709
  if (birth.place) {
629
- lines.push(`2 PLAC ${birth.place}`);
710
+ lines.push(`2 PLAC ${sanitizeGedcomValue(birth.place)}`);
630
711
  }
631
712
  }
632
713
  const death = extractFact(personData, "DEATH");
633
714
  if (death) {
634
715
  lines.push("1 DEAT");
635
716
  if (death.date) {
636
- lines.push(`2 DATE ${death.date}`);
717
+ lines.push(`2 DATE ${sanitizeGedcomValue(death.date)}`);
637
718
  }
638
719
  if (death.place) {
639
- lines.push(`2 PLAC ${death.place}`);
720
+ lines.push(`2 PLAC ${sanitizeGedcomValue(death.place)}`);
640
721
  }
641
722
  }
642
723
  personData.facts?.forEach((fact) => {
@@ -648,8 +729,7 @@ function convertToGedcom(pedigreeData, options = {}) {
648
729
  if (includeNotes && person.notes?.persons?.[0]?.notes) {
649
730
  person.notes.persons[0].notes.forEach((note) => {
650
731
  if (note.text) {
651
- const noteText = note.text.replace(/\n/g, " ");
652
- lines.push(`1 NOTE ${noteText}`);
732
+ lines.push(`1 NOTE ${sanitizeGedcomValue(note.text)}`);
653
733
  }
654
734
  });
655
735
  }
@@ -667,7 +747,7 @@ function convertToGedcom(pedigreeData, options = {}) {
667
747
  sourceRef.qualifiers.forEach((qualifier) => {
668
748
  if (qualifier.name && qualifier.value) {
669
749
  lines.push(
670
- `2 NOTE ${qualifier.name}: ${qualifier.value}`
750
+ `2 NOTE ${sanitizeGedcomValue(`${qualifier.name}: ${qualifier.value}`)}`
671
751
  );
672
752
  }
673
753
  });
@@ -821,31 +901,36 @@ function addMarriageFacts(lines, relationships, person1, person2) {
821
901
  return [a, b].sort().join("-") === relKey;
822
902
  });
823
903
  if (!rel) return;
904
+ const isMarriageEvent = (type) => !!type && (type.includes("Marriage") || type.includes("Divorce"));
824
905
  const marriageFacts = [];
825
906
  if (rel.facts && Array.isArray(rel.facts)) {
826
907
  rel.facts.forEach((f) => {
827
- if (f.type?.includes("Marriage")) {
908
+ if (isMarriageEvent(f.type)) {
828
909
  marriageFacts.push(f);
829
910
  }
830
911
  });
831
912
  }
832
913
  if (rel.details?.facts && Array.isArray(rel.details.facts)) {
833
914
  marriageFacts.push(
834
- ...rel.details.facts.filter((f) => f.type?.includes("Marriage"))
915
+ ...rel.details.facts.filter((f) => isMarriageEvent(f.type))
835
916
  );
836
917
  }
837
918
  if (rel.details?.persons && Array.isArray(rel.details.persons)) {
838
919
  rel.details.persons.forEach((p) => {
839
920
  if (p.facts && Array.isArray(p.facts)) {
840
921
  p.facts.forEach((f) => {
841
- if (f.type?.includes("Marriage")) {
922
+ if (isMarriageEvent(f.type)) {
842
923
  marriageFacts.push(f);
843
924
  }
844
925
  });
845
926
  }
846
927
  });
847
928
  }
929
+ const seenFacts = /* @__PURE__ */ new Set();
848
930
  marriageFacts.forEach((mf) => {
931
+ const key = `${mf.type}|${resolveFactDate(mf.date) || ""}|${mf.place?.original || ""}`;
932
+ if (seenFacts.has(key)) return;
933
+ seenFacts.add(key);
849
934
  const factLines = convertFactToGedcom(mf);
850
935
  factLines.forEach((line) => lines.push(line));
851
936
  });