@treeviz/gedcom-parser 2.1.28 → 2.1.30

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.
package/dist/cli/index.js CHANGED
@@ -140,6 +140,100 @@ var objeXrefKey = (obje, fallback = 0) => {
140
140
  return `@O${fallback}@`;
141
141
  };
142
142
  var objeNoteValue = (obje) => mediaIdValue(obje, "FILE.NOTE") || mediaIdValue(obje, "NOTE") || "";
143
+ var FS_WEB_RE = /familysearch\.org/i;
144
+ var FS_ARK_ID_RE = /^(?:ark:\/[^/]+\/)?([123]:[12]:[A-Za-z0-9-]+)$/i;
145
+ var listItems = (list) => {
146
+ if (!list) {
147
+ return [];
148
+ }
149
+ if (typeof list.values === "function") {
150
+ return list.values();
151
+ }
152
+ if (typeof list.forEach === "function") {
153
+ const items = [];
154
+ list.forEach((item) => items.push(item));
155
+ return items;
156
+ }
157
+ return [];
158
+ };
159
+ var toList = (node) => {
160
+ if (!node) {
161
+ return void 0;
162
+ }
163
+ const maybe = node;
164
+ if (typeof maybe.toList === "function") {
165
+ return maybe.toList();
166
+ }
167
+ if (typeof maybe.values === "function" || typeof maybe.forEach === "function") {
168
+ return maybe;
169
+ }
170
+ return void 0;
171
+ };
172
+ var normalizeFsWebUrl = (url) => {
173
+ const trimmed = url.trim();
174
+ if (!trimmed || !FS_WEB_RE.test(trimmed)) {
175
+ return void 0;
176
+ }
177
+ if (/\/dist\.jpg|\/service\/records\/storage\/|\/platform\//i.test(trimmed)) {
178
+ return void 0;
179
+ }
180
+ return trimmed;
181
+ };
182
+ var familySearchMediaOriginUrl = (obje, indi) => {
183
+ if (!obje) {
184
+ return void 0;
185
+ }
186
+ const resolved = obje.ref && typeof obje.toValue?.() === "string" ? obje.ref : obje;
187
+ const directWww = mediaIdValue(resolved, "WWW") || mediaIdValue(obje, "WWW");
188
+ const fromWww = directWww ? normalizeFsWebUrl(directWww) : void 0;
189
+ if (fromWww) {
190
+ return fromWww;
191
+ }
192
+ const objeKey = objeXrefKey(obje);
193
+ const sourItems = listItems(toList(indi?.get?.("SOUR")));
194
+ for (const sour of sourItems) {
195
+ if (!sour) continue;
196
+ const sourWww = mediaIdValue(sour, "WWW");
197
+ const normalized = sourWww ? normalizeFsWebUrl(sourWww) : void 0;
198
+ if (!normalized) continue;
199
+ const sourIsFs = mediaIdValue(sour, "_IS_FS") === "Y" || FS_WEB_RE.test(normalized);
200
+ if (!sourIsFs) continue;
201
+ let linksThisObje = false;
202
+ for (const ref of listItems(toList(sour.get?.("OBJE")))) {
203
+ if (ref && objeXrefKey(ref) === objeKey) {
204
+ linksThisObje = true;
205
+ break;
206
+ }
207
+ }
208
+ const sourFsId = mediaIdValue(sour, "_FS_ID");
209
+ const objeFsId = mediaIdValue(resolved, "_FS_ID") || mediaIdValue(obje, "_FS_ID");
210
+ if (linksThisObje || sourFsId && objeFsId && sourFsId === objeFsId) {
211
+ return normalized;
212
+ }
213
+ }
214
+ const isFs = mediaIdValue(resolved, "_IS_FS") === "Y" || mediaIdValue(obje, "_IS_FS") === "Y";
215
+ const fsId = mediaIdValue(resolved, "_FS_ID") || mediaIdValue(obje, "_FS_ID");
216
+ if (!isFs || !fsId) {
217
+ return void 0;
218
+ }
219
+ const arkMatch = fsId.match(FS_ARK_ID_RE);
220
+ if (arkMatch?.[1]) {
221
+ return `https://www.familysearch.org/ark:/61903/${arkMatch[1]}`;
222
+ }
223
+ if (/^\d+$/.test(fsId)) {
224
+ return `https://www.familysearch.org/photos/artifacts/${fsId}`;
225
+ }
226
+ for (const sour of sourItems) {
227
+ if (!sour) continue;
228
+ if (mediaIdValue(sour, "_FS_ID") !== fsId) continue;
229
+ const sourWww = mediaIdValue(sour, "WWW");
230
+ const normalized = sourWww ? normalizeFsWebUrl(sourWww) : void 0;
231
+ if (normalized) {
232
+ return normalized;
233
+ }
234
+ }
235
+ return void 0;
236
+ };
143
237
  var List = class _List {
144
238
  // length = 0;
145
239
  constructor(items) {
@@ -2257,6 +2351,14 @@ var mergeMediaListsByObjeKey = (...lists) => {
2257
2351
  for (const [id, item] of Object.entries(list)) {
2258
2352
  const objeKey = item.key ? String(item.key) : "";
2259
2353
  if (objeKey && seen.has(objeKey)) {
2354
+ if (item.originUrl) {
2355
+ const existing = Object.values(merged).find(
2356
+ (entry) => entry.key === objeKey
2357
+ );
2358
+ if (existing && !existing.originUrl) {
2359
+ existing.originUrl = item.originUrl;
2360
+ }
2361
+ }
2260
2362
  continue;
2261
2363
  }
2262
2364
  merged[id] = item;
@@ -3470,6 +3572,10 @@ var Indi = class extends Common {
3470
3572
  }
3471
3573
  if (url && imgId) {
3472
3574
  const id = `${tree || "gedzip"}-${this.id}-${imgId}`;
3575
+ const originUrl = familySearchMediaOriginUrl(
3576
+ objeRef ?? obje,
3577
+ this
3578
+ );
3473
3579
  list[id] = {
3474
3580
  key,
3475
3581
  isPrimary,
@@ -3481,7 +3587,8 @@ var Indi = class extends Common {
3481
3587
  note,
3482
3588
  url,
3483
3589
  contentType: type,
3484
- downloadName: `${this.id.replaceAll("@", "")}_${this.toNaturalName()?.replaceAll(" ", "-") || ""}_${(title || key.replaceAll("@", "").toString()).replaceAll(" ", "-")}`
3590
+ downloadName: `${this.id.replaceAll("@", "")}_${this.toNaturalName()?.replaceAll(" ", "-") || ""}_${(title || key.replaceAll("@", "").toString()).replaceAll(" ", "-")}`,
3591
+ ...originUrl ? { originUrl } : {}
3485
3592
  };
3486
3593
  }
3487
3594
  })
@@ -3701,18 +3808,39 @@ var Indi = class extends Common {
3701
3808
  if (!this.id) {
3702
3809
  return list;
3703
3810
  }
3704
- const objeList = this.get("OBJE")?.toList().copy();
3811
+ let objeList = this.get("OBJE")?.toList().copy();
3812
+ const sourList = this.get("SOUR")?.toList();
3813
+ sourList?.forEach((sour) => {
3814
+ const sourObje = sour?.get("OBJE")?.toList();
3815
+ if (!sourObje?.length) {
3816
+ return;
3817
+ }
3818
+ if (!objeList) {
3819
+ objeList = sourObje.copy();
3820
+ } else {
3821
+ objeList.merge(sourObje);
3822
+ }
3823
+ });
3705
3824
  if (!objeList || objeList.length === 0) {
3706
3825
  return list;
3707
3826
  }
3708
3827
  const rfn = this.get("RFN")?.toValue();
3709
3828
  const treeId = this.getUniversalTreeId() || rfn || "universal";
3829
+ const seenFsIds = /* @__PURE__ */ new Set();
3710
3830
  objeList.forEach((obje, index) => {
3711
3831
  if (!obje) {
3712
3832
  return;
3713
3833
  }
3714
3834
  const key = objeXrefKey(obje, index);
3715
3835
  obje.standardizeMedia();
3836
+ const fsId = obje?.get("_FS_ID")?.toValue?.();
3837
+ if (typeof fsId === "string" && fsId.trim()) {
3838
+ const normalized = fsId.trim();
3839
+ if (seenFsIds.has(normalized)) {
3840
+ return;
3841
+ }
3842
+ seenFsIds.add(normalized);
3843
+ }
3716
3844
  const isPrimary = obje?.get("_PRIM")?.toValue() === "Y";
3717
3845
  const url = obje?.get("FILE")?.toValue();
3718
3846
  const title = obje?.get("FILE.TITL")?.toValue() ?? obje?.get("TITL")?.toValue() ?? "";
@@ -3720,6 +3848,7 @@ var Indi = class extends Common {
3720
3848
  if (url) {
3721
3849
  const imgId = `media-${index}-${url.startsWith("data:") ? getFileExtension(url) || "bin" : url.split("/").pop()?.split("?")[0]?.substring(0, 20) || Date.now().toString(36)}`;
3722
3850
  const id = `${treeId}-${this.id}-${imgId}`;
3851
+ const originUrl = familySearchMediaOriginUrl(obje, this);
3723
3852
  list[id] = {
3724
3853
  isPrimary,
3725
3854
  key,
@@ -3731,7 +3860,8 @@ var Indi = class extends Common {
3731
3860
  note: objeNoteValue(obje),
3732
3861
  url,
3733
3862
  contentType: type,
3734
- downloadName: `${this.id.replaceAll("@", "")}_${this.toNaturalName()?.replaceAll(" ", "-") || ""}_${(title || key.replaceAll("@", "").toString()).replaceAll(" ", "-")}`
3863
+ downloadName: `${this.id.replaceAll("@", "")}_${this.toNaturalName()?.replaceAll(" ", "-") || ""}_${(title || key.replaceAll("@", "").toString()).replaceAll(" ", "-")}`,
3864
+ ...originUrl ? { originUrl } : {}
3735
3865
  };
3736
3866
  }
3737
3867
  });
@@ -3757,7 +3887,23 @@ var Indi = class extends Common {
3757
3887
  lists.push(this.geniMedia());
3758
3888
  }
3759
3889
  lists.push(this.universalMedia());
3760
- return mergeMediaListsByObjeKey(...lists);
3890
+ const merged = mergeMediaListsByObjeKey(...lists);
3891
+ const seenFsIds = /* @__PURE__ */ new Set();
3892
+ const deduped = {};
3893
+ for (const [id, media] of Object.entries(merged ?? {})) {
3894
+ const xref = media.key ? String(media.key) : "";
3895
+ const top = xref && this._gedcom ? this._gedcom.obje(xref) : void 0;
3896
+ const fsId = top?.get("_FS_ID")?.toValue?.();
3897
+ if (typeof fsId === "string" && fsId.trim()) {
3898
+ const normalized = fsId.trim();
3899
+ if (seenFsIds.has(normalized)) {
3900
+ continue;
3901
+ }
3902
+ seenFsIds.add(normalized);
3903
+ }
3904
+ deduped[id] = media;
3905
+ }
3906
+ return deduped;
3761
3907
  }
3762
3908
  async getProfilePicture(namespace, onlyPrimary = true) {
3763
3909
  if (!this.id) {
@@ -9279,7 +9425,7 @@ var ensureExtensionSchema = (gedcom, extraTags) => {
9279
9425
 
9280
9426
  // package.json
9281
9427
  var package_default = {
9282
- version: "2.1.28"};
9428
+ version: "2.1.30"};
9283
9429
 
9284
9430
  // src/utils/get-product-details.ts
9285
9431
  var isDevelopment = () => {
@@ -10319,26 +10465,47 @@ var mergeGedcoms = (targetGedcom, sourceGedcom, strategy = "id") => {
10319
10465
  const mergedGedcom = targetGedcom;
10320
10466
  const idMap = /* @__PURE__ */ new Map();
10321
10467
  const matchMap = /* @__PURE__ */ new Map();
10468
+ const reusedTopLevelIds = /* @__PURE__ */ new Set();
10322
10469
  const isXref = (value) => !!value && /^@[A-Za-z0-9_]+@$/.test(value.trim());
10470
+ const recordFsId = (item) => {
10471
+ const node = item;
10472
+ const value = node?.get?.("_FS_ID")?.toValue?.();
10473
+ return typeof value === "string" ? value.trim() : "";
10474
+ };
10323
10475
  const allocateTopLevelIds = (sourceList, targetList, prefix) => {
10324
10476
  const used = /* @__PURE__ */ new Set();
10325
- targetList?.forEach((_item, id) => {
10477
+ const byFsId = /* @__PURE__ */ new Map();
10478
+ targetList?.forEach((item, id) => {
10326
10479
  used.add(String(id));
10480
+ const fsId = recordFsId(item);
10481
+ if (fsId && !byFsId.has(fsId)) {
10482
+ byFsId.set(fsId, String(id));
10483
+ }
10327
10484
  });
10328
10485
  idMap.forEach((mapped) => {
10329
10486
  used.add(String(mapped));
10330
10487
  });
10331
- sourceList?.forEach((_item, sourceId) => {
10488
+ sourceList?.forEach((item, sourceId) => {
10332
10489
  const sid = String(sourceId);
10333
10490
  if (!sid || idMap.has(sid)) {
10334
10491
  return;
10335
10492
  }
10493
+ const fsId = recordFsId(item);
10494
+ if (fsId && byFsId.has(fsId)) {
10495
+ const existingId = byFsId.get(fsId);
10496
+ idMap.set(sid, existingId);
10497
+ reusedTopLevelIds.add(sid);
10498
+ return;
10499
+ }
10336
10500
  let newId = sid;
10337
10501
  if (used.has(sid)) {
10338
10502
  newId = nextRecordId([...used], prefix);
10339
10503
  }
10340
10504
  used.add(newId);
10341
10505
  idMap.set(sid, newId);
10506
+ if (fsId) {
10507
+ byFsId.set(fsId, newId);
10508
+ }
10342
10509
  });
10343
10510
  };
10344
10511
  const remapIndiPointers = (clonedIndi, sourceIndi, tag) => {
@@ -10358,6 +10525,9 @@ var mergeGedcoms = (targetGedcom, sourceGedcom, strategy = "id") => {
10358
10525
  }
10359
10526
  const pointer = createCommon(mergedGedcom, void 0, clonedIndi);
10360
10527
  pointer.value = newVal;
10528
+ if (tag === "OBJE" || tag === "SOUR") {
10529
+ pointer.refType = tag;
10530
+ }
10361
10531
  clonedIndi.assign(tag, pointer, true);
10362
10532
  });
10363
10533
  };
@@ -10469,10 +10639,39 @@ var mergeGedcoms = (targetGedcom, sourceGedcom, strategy = "id") => {
10469
10639
  clonedFam.setGedcom(mergedGedcom);
10470
10640
  clonedFams.set(sourceFam.id, clonedFam);
10471
10641
  });
10642
+ const fillMissingTag = (target, source, tag) => {
10643
+ const current = target.get(tag)?.toValue?.();
10644
+ if (current !== void 0 && current !== null && String(current).trim()) {
10645
+ return;
10646
+ }
10647
+ const next = source.get(tag)?.toValue?.();
10648
+ if (typeof next === "string" && next.trim()) {
10649
+ target.set(tag, next);
10650
+ }
10651
+ };
10652
+ const isBinaryMediaUrl = (url) => /\.(jpe?g|png|gif|webp|tif{1,2}|pdf)(\?|$)/i.test(url) || /dist\.jpg/i.test(url);
10472
10653
  sourceGedcom.objes()?.forEach((sourceObje, sourceId) => {
10473
10654
  if (!sourceObje || !sourceId) return;
10655
+ const sid = String(sourceId);
10474
10656
  const newId = idMap.get(sourceId);
10475
10657
  if (!newId) return;
10658
+ if (reusedTopLevelIds.has(sid)) {
10659
+ const existingObje = mergedGedcom.obje(newId);
10660
+ if (existingObje) {
10661
+ fillMissingTag(existingObje, sourceObje, "WWW");
10662
+ fillMissingTag(existingObje, sourceObje, "TITL");
10663
+ const curFile = String(
10664
+ existingObje.get("FILE")?.toValue?.() ?? ""
10665
+ ).trim();
10666
+ const nextFile = String(
10667
+ sourceObje.get("FILE")?.toValue?.() ?? ""
10668
+ ).trim();
10669
+ if (nextFile && (!curFile || isBinaryMediaUrl(nextFile) && !isBinaryMediaUrl(curFile))) {
10670
+ existingObje.set("FILE", nextFile);
10671
+ }
10672
+ }
10673
+ return;
10674
+ }
10476
10675
  const cloned = sourceObje.clone(false);
10477
10676
  cloned.id = newId;
10478
10677
  cloned.setGedcom(mergedGedcom);
@@ -10485,11 +10684,35 @@ var mergeGedcoms = (targetGedcom, sourceGedcom, strategy = "id") => {
10485
10684
  });
10486
10685
  sourceGedcom.sours()?.forEach((sourceSour, sourceId) => {
10487
10686
  if (!sourceSour || !sourceId) return;
10687
+ const sid = String(sourceId);
10488
10688
  const newId = idMap.get(sourceId);
10489
10689
  if (!newId) return;
10690
+ if (reusedTopLevelIds.has(sid)) {
10691
+ const existingSour = mergedGedcom.sour(newId);
10692
+ if (existingSour) {
10693
+ fillMissingTag(existingSour, sourceSour, "WWW");
10694
+ fillMissingTag(existingSour, sourceSour, "TITL");
10695
+ fillMissingTag(existingSour, sourceSour, "PAGE");
10696
+ }
10697
+ return;
10698
+ }
10490
10699
  const cloned = sourceSour.clone(false);
10491
10700
  cloned.id = newId;
10492
10701
  cloned.setGedcom(mergedGedcom);
10702
+ const sourObjes = cloned.get("OBJE")?.toList();
10703
+ if (sourObjes?.length) {
10704
+ cloned.remove("OBJE");
10705
+ sourObjes.forEach((ref) => {
10706
+ const oldVal = String(ref?.toValue?.() ?? "").trim();
10707
+ if (!oldVal) return;
10708
+ const newVal = isXref(oldVal) ? idMap.get(oldVal) : oldVal;
10709
+ if (!newVal) return;
10710
+ const pointer = createCommon(mergedGedcom, void 0, cloned);
10711
+ pointer.value = newVal;
10712
+ pointer.refType = "OBJE";
10713
+ cloned.assign("OBJE", pointer, true);
10714
+ });
10715
+ }
10493
10716
  const existing = mergedGedcom.sours();
10494
10717
  const list = existing ?? new Sources();
10495
10718
  if (!existing) {
@@ -10513,6 +10736,7 @@ var mergeGedcoms = (targetGedcom, sourceGedcom, strategy = "id") => {
10513
10736
  clonedIndi
10514
10737
  );
10515
10738
  newFamRef.value = newFamId;
10739
+ newFamRef.refType = "FAM";
10516
10740
  clonedIndi.assign("FAMS", newFamRef, true);
10517
10741
  }
10518
10742
  }
@@ -10530,6 +10754,7 @@ var mergeGedcoms = (targetGedcom, sourceGedcom, strategy = "id") => {
10530
10754
  clonedIndi
10531
10755
  );
10532
10756
  newFamRef.value = newFamId;
10757
+ newFamRef.refType = "FAM";
10533
10758
  clonedIndi.assign("FAMC", newFamRef, true);
10534
10759
  }
10535
10760
  }
@@ -10550,6 +10775,7 @@ var mergeGedcoms = (targetGedcom, sourceGedcom, strategy = "id") => {
10550
10775
  clonedFam
10551
10776
  );
10552
10777
  newHusbRef.value = newHusbId;
10778
+ newHusbRef.refType = "INDI";
10553
10779
  clonedFam.set("HUSB", newHusbRef);
10554
10780
  }
10555
10781
  }
@@ -10563,6 +10789,7 @@ var mergeGedcoms = (targetGedcom, sourceGedcom, strategy = "id") => {
10563
10789
  clonedFam
10564
10790
  );
10565
10791
  newWifeRef.value = newWifeId;
10792
+ newWifeRef.refType = "INDI";
10566
10793
  clonedFam.set("WIFE", newWifeRef);
10567
10794
  }
10568
10795
  }
@@ -10579,6 +10806,7 @@ var mergeGedcoms = (targetGedcom, sourceGedcom, strategy = "id") => {
10579
10806
  clonedFam
10580
10807
  );
10581
10808
  newChildRef.value = newChildId;
10809
+ newChildRef.refType = "INDI";
10582
10810
  clonedFam.assign("CHIL", newChildRef, true);
10583
10811
  }
10584
10812
  }
@@ -10840,6 +11068,9 @@ var GedcomTree = {
10840
11068
  }, []);
10841
11069
  let linesJoined = lines.join("\n");
10842
11070
  if (purgeOversized && typeof maxFileSizeToPurge === "number" && !linesJoined.includes("1 _IS_PURGED true") && getRawSize(linesJoined) > maxFileSizeToPurge) {
11071
+ if (!linesJoined.endsWith("\n")) {
11072
+ linesJoined += "\n";
11073
+ }
10843
11074
  linesJoined = linesJoined.replace(
10844
11075
  /1\s(NOTE|OBJE|SOUR)([^\n]*\n)([23456789]\s.+\n)*/gm,
10845
11076
  ""
@@ -464,6 +464,100 @@ var objeXrefKey = (obje, fallback = 0) => {
464
464
  return `@O${fallback}@`;
465
465
  };
466
466
  var objeNoteValue = (obje) => mediaIdValue(obje, "FILE.NOTE") || mediaIdValue(obje, "NOTE") || "";
467
+ var FS_WEB_RE = /familysearch\.org/i;
468
+ var FS_ARK_ID_RE = /^(?:ark:\/[^/]+\/)?([123]:[12]:[A-Za-z0-9-]+)$/i;
469
+ var listItems = (list) => {
470
+ if (!list) {
471
+ return [];
472
+ }
473
+ if (typeof list.values === "function") {
474
+ return list.values();
475
+ }
476
+ if (typeof list.forEach === "function") {
477
+ const items = [];
478
+ list.forEach((item) => items.push(item));
479
+ return items;
480
+ }
481
+ return [];
482
+ };
483
+ var toList = (node) => {
484
+ if (!node) {
485
+ return void 0;
486
+ }
487
+ const maybe = node;
488
+ if (typeof maybe.toList === "function") {
489
+ return maybe.toList();
490
+ }
491
+ if (typeof maybe.values === "function" || typeof maybe.forEach === "function") {
492
+ return maybe;
493
+ }
494
+ return void 0;
495
+ };
496
+ var normalizeFsWebUrl = (url) => {
497
+ const trimmed = url.trim();
498
+ if (!trimmed || !FS_WEB_RE.test(trimmed)) {
499
+ return void 0;
500
+ }
501
+ if (/\/dist\.jpg|\/service\/records\/storage\/|\/platform\//i.test(trimmed)) {
502
+ return void 0;
503
+ }
504
+ return trimmed;
505
+ };
506
+ var familySearchMediaOriginUrl = (obje, indi) => {
507
+ if (!obje) {
508
+ return void 0;
509
+ }
510
+ const resolved = obje.ref && typeof obje.toValue?.() === "string" ? obje.ref : obje;
511
+ const directWww = mediaIdValue(resolved, "WWW") || mediaIdValue(obje, "WWW");
512
+ const fromWww = directWww ? normalizeFsWebUrl(directWww) : void 0;
513
+ if (fromWww) {
514
+ return fromWww;
515
+ }
516
+ const objeKey = objeXrefKey(obje);
517
+ const sourItems = listItems(toList(indi?.get?.("SOUR")));
518
+ for (const sour of sourItems) {
519
+ if (!sour) continue;
520
+ const sourWww = mediaIdValue(sour, "WWW");
521
+ const normalized = sourWww ? normalizeFsWebUrl(sourWww) : void 0;
522
+ if (!normalized) continue;
523
+ const sourIsFs = mediaIdValue(sour, "_IS_FS") === "Y" || FS_WEB_RE.test(normalized);
524
+ if (!sourIsFs) continue;
525
+ let linksThisObje = false;
526
+ for (const ref of listItems(toList(sour.get?.("OBJE")))) {
527
+ if (ref && objeXrefKey(ref) === objeKey) {
528
+ linksThisObje = true;
529
+ break;
530
+ }
531
+ }
532
+ const sourFsId = mediaIdValue(sour, "_FS_ID");
533
+ const objeFsId = mediaIdValue(resolved, "_FS_ID") || mediaIdValue(obje, "_FS_ID");
534
+ if (linksThisObje || sourFsId && objeFsId && sourFsId === objeFsId) {
535
+ return normalized;
536
+ }
537
+ }
538
+ const isFs = mediaIdValue(resolved, "_IS_FS") === "Y" || mediaIdValue(obje, "_IS_FS") === "Y";
539
+ const fsId = mediaIdValue(resolved, "_FS_ID") || mediaIdValue(obje, "_FS_ID");
540
+ if (!isFs || !fsId) {
541
+ return void 0;
542
+ }
543
+ const arkMatch = fsId.match(FS_ARK_ID_RE);
544
+ if (arkMatch?.[1]) {
545
+ return `https://www.familysearch.org/ark:/61903/${arkMatch[1]}`;
546
+ }
547
+ if (/^\d+$/.test(fsId)) {
548
+ return `https://www.familysearch.org/photos/artifacts/${fsId}`;
549
+ }
550
+ for (const sour of sourItems) {
551
+ if (!sour) continue;
552
+ if (mediaIdValue(sour, "_FS_ID") !== fsId) continue;
553
+ const sourWww = mediaIdValue(sour, "WWW");
554
+ const normalized = sourWww ? normalizeFsWebUrl(sourWww) : void 0;
555
+ if (normalized) {
556
+ return normalized;
557
+ }
558
+ }
559
+ return void 0;
560
+ };
467
561
 
468
562
  // src/classes/common.ts
469
563
  var Common = class _Common {
@@ -2392,6 +2486,14 @@ var mergeMediaListsByObjeKey = (...lists) => {
2392
2486
  for (const [id, item] of Object.entries(list)) {
2393
2487
  const objeKey = item.key ? String(item.key) : "";
2394
2488
  if (objeKey && seen.has(objeKey)) {
2489
+ if (item.originUrl) {
2490
+ const existing = Object.values(merged).find(
2491
+ (entry) => entry.key === objeKey
2492
+ );
2493
+ if (existing && !existing.originUrl) {
2494
+ existing.originUrl = item.originUrl;
2495
+ }
2496
+ }
2395
2497
  continue;
2396
2498
  }
2397
2499
  merged[id] = item;
@@ -3706,6 +3808,10 @@ var Indi = class extends Common {
3706
3808
  }
3707
3809
  if (url && imgId) {
3708
3810
  const id = `${tree || "gedzip"}-${this.id}-${imgId}`;
3811
+ const originUrl = familySearchMediaOriginUrl(
3812
+ objeRef ?? obje,
3813
+ this
3814
+ );
3709
3815
  list[id] = {
3710
3816
  key,
3711
3817
  isPrimary,
@@ -3717,7 +3823,8 @@ var Indi = class extends Common {
3717
3823
  note,
3718
3824
  url,
3719
3825
  contentType: type,
3720
- downloadName: `${this.id.replaceAll("@", "")}_${this.toNaturalName()?.replaceAll(" ", "-") || ""}_${(title || key.replaceAll("@", "").toString()).replaceAll(" ", "-")}`
3826
+ downloadName: `${this.id.replaceAll("@", "")}_${this.toNaturalName()?.replaceAll(" ", "-") || ""}_${(title || key.replaceAll("@", "").toString()).replaceAll(" ", "-")}`,
3827
+ ...originUrl ? { originUrl } : {}
3721
3828
  };
3722
3829
  }
3723
3830
  })
@@ -3937,18 +4044,39 @@ var Indi = class extends Common {
3937
4044
  if (!this.id) {
3938
4045
  return list;
3939
4046
  }
3940
- const objeList = this.get("OBJE")?.toList().copy();
4047
+ let objeList = this.get("OBJE")?.toList().copy();
4048
+ const sourList = this.get("SOUR")?.toList();
4049
+ sourList?.forEach((sour) => {
4050
+ const sourObje = sour?.get("OBJE")?.toList();
4051
+ if (!sourObje?.length) {
4052
+ return;
4053
+ }
4054
+ if (!objeList) {
4055
+ objeList = sourObje.copy();
4056
+ } else {
4057
+ objeList.merge(sourObje);
4058
+ }
4059
+ });
3941
4060
  if (!objeList || objeList.length === 0) {
3942
4061
  return list;
3943
4062
  }
3944
4063
  const rfn = this.get("RFN")?.toValue();
3945
4064
  const treeId = this.getUniversalTreeId() || rfn || "universal";
4065
+ const seenFsIds = /* @__PURE__ */ new Set();
3946
4066
  objeList.forEach((obje, index) => {
3947
4067
  if (!obje) {
3948
4068
  return;
3949
4069
  }
3950
4070
  const key = objeXrefKey(obje, index);
3951
4071
  obje.standardizeMedia();
4072
+ const fsId = obje?.get("_FS_ID")?.toValue?.();
4073
+ if (typeof fsId === "string" && fsId.trim()) {
4074
+ const normalized = fsId.trim();
4075
+ if (seenFsIds.has(normalized)) {
4076
+ return;
4077
+ }
4078
+ seenFsIds.add(normalized);
4079
+ }
3952
4080
  const isPrimary = obje?.get("_PRIM")?.toValue() === "Y";
3953
4081
  const url = obje?.get("FILE")?.toValue();
3954
4082
  const title = obje?.get("FILE.TITL")?.toValue() ?? obje?.get("TITL")?.toValue() ?? "";
@@ -3956,6 +4084,7 @@ var Indi = class extends Common {
3956
4084
  if (url) {
3957
4085
  const imgId = `media-${index}-${url.startsWith("data:") ? getFileExtension(url) || "bin" : url.split("/").pop()?.split("?")[0]?.substring(0, 20) || Date.now().toString(36)}`;
3958
4086
  const id = `${treeId}-${this.id}-${imgId}`;
4087
+ const originUrl = familySearchMediaOriginUrl(obje, this);
3959
4088
  list[id] = {
3960
4089
  isPrimary,
3961
4090
  key,
@@ -3967,7 +4096,8 @@ var Indi = class extends Common {
3967
4096
  note: objeNoteValue(obje),
3968
4097
  url,
3969
4098
  contentType: type,
3970
- downloadName: `${this.id.replaceAll("@", "")}_${this.toNaturalName()?.replaceAll(" ", "-") || ""}_${(title || key.replaceAll("@", "").toString()).replaceAll(" ", "-")}`
4099
+ downloadName: `${this.id.replaceAll("@", "")}_${this.toNaturalName()?.replaceAll(" ", "-") || ""}_${(title || key.replaceAll("@", "").toString()).replaceAll(" ", "-")}`,
4100
+ ...originUrl ? { originUrl } : {}
3971
4101
  };
3972
4102
  }
3973
4103
  });
@@ -3993,7 +4123,23 @@ var Indi = class extends Common {
3993
4123
  lists.push(this.geniMedia());
3994
4124
  }
3995
4125
  lists.push(this.universalMedia());
3996
- return mergeMediaListsByObjeKey(...lists);
4126
+ const merged = mergeMediaListsByObjeKey(...lists);
4127
+ const seenFsIds = /* @__PURE__ */ new Set();
4128
+ const deduped = {};
4129
+ for (const [id, media] of Object.entries(merged ?? {})) {
4130
+ const xref = media.key ? String(media.key) : "";
4131
+ const top = xref && this._gedcom ? this._gedcom.obje(xref) : void 0;
4132
+ const fsId = top?.get("_FS_ID")?.toValue?.();
4133
+ if (typeof fsId === "string" && fsId.trim()) {
4134
+ const normalized = fsId.trim();
4135
+ if (seenFsIds.has(normalized)) {
4136
+ continue;
4137
+ }
4138
+ seenFsIds.add(normalized);
4139
+ }
4140
+ deduped[id] = media;
4141
+ }
4142
+ return deduped;
3997
4143
  }
3998
4144
  async getProfilePicture(namespace, onlyPrimary = true) {
3999
4145
  if (!this.id) {