@treeviz/familysearch-sdk 2.0.6 → 2.0.8

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