gedcom-ts 2.0.2 → 2026.5.0

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +175 -0
  2. package/README.md +460 -88
  3. package/dist/commons/Act.d.ts +46 -7
  4. package/dist/commons/DateAct.d.ts +40 -2
  5. package/dist/commons/Identifier.enum.d.ts +3 -0
  6. package/dist/commons/IndiAttribute.d.ts +11 -0
  7. package/dist/commons/IndiGedcomSubLine.d.ts +6 -0
  8. package/dist/commons/MultimediaFile.d.ts +13 -2
  9. package/dist/commons/Person.d.ts +27 -4
  10. package/dist/commons/PersonNameVariant.d.ts +21 -0
  11. package/dist/commons/Place.d.ts +19 -0
  12. package/dist/commons/clonePrimitives.d.ts +17 -0
  13. package/dist/commons/gedcomEventTags.d.ts +17 -0
  14. package/dist/dataset/ReadGedEdit.d.ts +27 -0
  15. package/dist/dataset/cloneModels.d.ts +8 -0
  16. package/dist/dataset/graphOps.d.ts +29 -0
  17. package/dist/dataset/index.d.ts +9 -0
  18. package/dist/dataset/readGedCommands.d.ts +46 -0
  19. package/dist/dataset/readGedMutations.d.ts +12 -0
  20. package/dist/dataset/validation.d.ts +36 -0
  21. package/dist/edit/ActEdit.d.ts +37 -0
  22. package/dist/edit/ActMediaEdit.d.ts +21 -0
  23. package/dist/edit/ActsEdit.d.ts +34 -0
  24. package/dist/edit/DateActEdit.d.ts +37 -0
  25. package/dist/edit/GedcomExportOptionsEdit.d.ts +20 -0
  26. package/dist/edit/IndiAttributesEdit.d.ts +38 -0
  27. package/dist/edit/MultimediaFileEdit.d.ts +17 -0
  28. package/dist/edit/NameVariantsEdit.d.ts +43 -0
  29. package/dist/edit/NoteEdit.d.ts +18 -0
  30. package/dist/edit/NotesEdit.d.ts +25 -0
  31. package/dist/edit/PersonEdit.d.ts +47 -0
  32. package/dist/edit/PersonMediaEdit.d.ts +27 -0
  33. package/dist/edit/PlaceEdit.d.ts +23 -0
  34. package/dist/edit/factories.d.ts +35 -0
  35. package/dist/edit/index.d.ts +33 -0
  36. package/dist/export/GEDCOM.d.ts +33 -4
  37. package/dist/import/LoadFile.d.ts +14 -0
  38. package/dist/import/PreservedRecordsBuffer.d.ts +21 -0
  39. package/dist/import/ReadGed.d.ts +49 -0
  40. package/dist/import/SplitedInformations.d.ts +35 -3
  41. package/dist/index.cjs +1 -1
  42. package/dist/index.d.ts +17 -4
  43. package/dist/index.mjs +1 -1
  44. package/dist/utils/gedcom/actExtraction.d.ts +3 -1
  45. package/dist/utils/gedcom/datasetVersion.d.ts +7 -0
  46. package/dist/utils/gedcom/extractIndiNamesAndAttributes.d.ts +4 -0
  47. package/dist/utils/gedcom/importGedcomNote.d.ts +17 -0
  48. package/dist/utils/gedcom/labelKeyedRecords.d.ts +13 -0
  49. package/dist/utils/gedcom/mediaFormFromUri.d.ts +2 -0
  50. package/dist/utils/gedcom/parseStandaloneObje.d.ts +9 -0
  51. package/dist/utils/gedcom/personName.d.ts +7 -0
  52. package/dist/utils/gedcom/pointers.d.ts +16 -0
  53. package/dist/utils/gedcom/uriBasename.d.ts +2 -0
  54. package/dist/utils/multimedia/registerTrackedMedia.d.ts +10 -0
  55. package/dist/version.d.ts +6 -0
  56. package/package.json +1 -1
package/README.md CHANGED
@@ -2,13 +2,20 @@
2
2
 
3
3
  `gedcom-ts` is a browser-oriented TypeScript library to:
4
4
 
5
- - import genealogy data from GEDCOM (`.ged`) or ZIP (`.zip`)
6
- - work with a typed JSON model (persons, acts, notes, media, places)
7
- - export data back to GEDCOM (`.ged`) or ZIP (`.zip`)
5
+ - import genealogy data from GEDCOM (`.ged`) or ZIP (`.zip` / `.gdz`)
6
+ - work with a typed JSON model (persons, acts, dates, places, notes, media, name variants, attributes)
7
+ - edit the model in-place through a fluent, chainable API (`editPerson`, `editAct`, …)
8
+ - export data back to GEDCOM (`.ged`) or GEDZIP (`.zip`)
9
+
10
+ ## Live demo
11
+
12
+ A graphical demo showcasing the public API (import a `.ged` / `.zip`, browse the typed model, export back) is available at **[https://gedcomts.jaunet.me](https://gedcomts.jaunet.me)**
8
13
 
9
14
  ## Project
10
15
 
11
16
  - NPM package: [gedcom-ts](https://www.npmjs.com/package/gedcom-ts)
17
+ - **Versioning (CalVer)** : releases use **`AAAA.M.micro`** (e.g. `2026.5.0` = May 2026). The npm `version`, `GEDCOM_LIBRARY_VERSION` (export `HEAD`.`SOUR`.`VERS`), and release branches share the same label. Older changelog entries still refer to semver (`2.1.0`, …). See [CHANGELOG.md](CHANGELOG.md).
18
+ - **GEDCOM 7 roadmap** (spec gedcom.io, coverage matrix, prioritized gaps): [docs/GEDCOM7-roadmap.md](docs/GEDCOM7-roadmap.md)
12
19
 
13
20
  ## Installation
14
21
 
@@ -19,58 +26,144 @@ npm install gedcom-ts
19
26
  ## Runtime Requirements
20
27
 
21
28
  - modern browser runtime (`File`, `Blob`, `XMLHttpRequest`, `URL.createObjectURL`)
22
- - for pure Node.js usage, DOM polyfills are required
29
+ - for pure Node.js usage, DOM polyfills are required (the library targets browsers)
23
30
 
24
- ## Import
31
+ ## Quick start
25
32
 
26
33
  ```ts
27
- import { importGedFile } from "gedcom-ts";
34
+ import { importGedFile, ExportGedzipFile } from "gedcom-ts";
28
35
 
29
- async function loadGenealogy(file: File) {
36
+ async function roundTrip(file: File) {
30
37
  const readGed = await importGedFile(file);
31
- return readGed.persons;
38
+ const persons = readGed.persons;
39
+
40
+ if (persons.length > 0) {
41
+ persons[0].lastname = persons[0].lastname.toUpperCase();
42
+ }
43
+
44
+ await new ExportGedzipFile("updated-tree", persons).download();
32
45
  }
33
46
  ```
34
47
 
35
- `importGedFile(file)` accepts:
48
+ Typical workflow:
49
+
50
+ 1. import a file with `importGedFile` (or start from `createEmptyReadGed()`)
51
+ 2. read / mutate the typed `Person`, `Act`, `Place`, `Note`, `MultimediaFile` objects (directly or through `editPerson` / `editAct` / `editPlace` / …)
52
+ 3. export as `.ged` (`ExportGedcomFile`) or `.zip` (`ExportGedzipFile`)
53
+
54
+ ## Public API reference
55
+
56
+ Everything exported from `gedcom-ts` is documented below with a short description and a minimal usage snippet. The full list mirrors the public exports of `src/index.ts`.
57
+
58
+ ### Importing a file
36
59
 
37
- - a GED file (`.ged`)
38
- - a ZIP containing one GED file + optional media files
60
+ #### `importGedFile(file: File): Promise<ReadGed>`
39
61
 
40
- It returns a `ReadGed` instance with parsed persons, families, notes, acts, media and maps.
62
+ Detects the format from the file MIME / extension and dispatches to the right reader.
41
63
 
42
- ## Utilisation
64
+ - accepts a single `.ged` file
65
+ - accepts a `.zip` / `.gdz` archive containing one `.ged` + optional media files
66
+ - throws `Error(IMPORT_ERR_ZIP_GED_UNREADABLE)` when the embedded `.ged` cannot be decoded (typical cause: password-protected ZIP)
43
67
 
44
68
  ```ts
45
- import { ReadGed, Person, createSosaMap } from "gedcom-ts";
69
+ import { importGedFile } from "gedcom-ts";
46
70
 
47
- function buildViewModel(readGed: ReadGed, root: Person) {
48
- const persons = readGed.persons;
49
- const personByIndi = readGed.mapPersons;
50
- const sosaMap = createSosaMap(root, readGed.partnersMap);
51
- return { persons, personByIndi, sosaMap };
71
+ const readGed = await importGedFile(fileInput.files![0]);
72
+ console.log(readGed.persons.length, readGed.datasetVersion);
73
+ ```
74
+
75
+ #### `createEmptyReadGed(options?): ReadGed`
76
+
77
+ Creates an empty graph with the same runtime shape as a successful import (empty `persons`, initialized `mapPersons` / `partnersMap` / `childsMap` / `placesMap` / `mapFiles`). Use it to start a brand-new tree without parsing a file. `datasetVersion` is set to `"7.0"`.
78
+
79
+ ```ts
80
+ import { createEmptyReadGed } from "gedcom-ts";
81
+
82
+ const readGed = createEmptyReadGed();
83
+ ```
84
+
85
+ #### `IMPORT_ERR_ZIP_GED_UNREADABLE: string`
86
+
87
+ Sentinel error message thrown by `importGedFile` when a `.ged` inside a ZIP cannot be decoded (typically because the ZIP is password-protected). Compare with `error.message === IMPORT_ERR_ZIP_GED_UNREADABLE` to display a tailored message.
88
+
89
+ ```ts
90
+ import { importGedFile, IMPORT_ERR_ZIP_GED_UNREADABLE } from "gedcom-ts";
91
+
92
+ try {
93
+ await importGedFile(file);
94
+ } catch (error) {
95
+ if (error instanceof Error && error.message === IMPORT_ERR_ZIP_GED_UNREADABLE) {
96
+ alert("Please unzip the archive manually and import the .ged file.");
97
+ }
52
98
  }
53
99
  ```
54
100
 
55
- Typical workflow:
101
+ #### `ReadGed`
102
+
103
+ Result of an import. Top-level GEDCOM records not modeled into the typed graph (`OBJE`, `REPO`, `SOUR`, `SUBM`, …) are kept in order on `readGed.preservedTopLevelRecords` for round-trip export.
104
+
105
+ Notable members:
106
+
107
+ | Member | Description |
108
+ | --- | --- |
109
+ | `persons: Person[]` | Imported individuals. |
110
+ | `mapPersons: Map<number, Person>` | `INDI` (integer) → `Person`. |
111
+ | `partnersMap: Map<number, Person[]>` | Family id → spouses. |
112
+ | `childsMap: Map<number, Person[]>` | Family id → children. |
113
+ | `placesMap: Map<string, Place>` | City name → `Place` (first occurrence wins). |
114
+ | `mapFiles: Map<string, File>` | Relative path → media `File` (for ZIP imports). |
115
+ | `datasetVersion: GedcomDatasetVersion` | `"7.0"`, `"5.5"` or `"unknown"`. |
116
+ | `preservedTopLevelRecords: string[]` | Raw blocks for partial round-trip. |
117
+ | `resolveIndividualPointer(raw)` | Resolves `@I12@`, `I12`, `@Homer_Simpson@`, `Homer_Simpson` to a `Person`. |
118
+ | `getChildrenForParent(parent)` | All children attached to a parent (via `FAMS`). |
119
+ | `getChildrenOfFamily(familyId)` | Children of a single family. |
120
+ | `groupPartners()` | Rebuilds `partnersMap` / `childsMap` after editing links. |
121
+ | `generateUniqueIndi()` | Next free `INDI` number for new persons. |
122
+ | `rehydratePlacesFromActs()` | Rebuilds `placesMap` from act places (e.g. after manual graph edits). Use `editReadGed(readGed).addPerson(...)` to register persons so maps stay coherent. |
56
123
 
57
- 1. import a file with `importGedFile`
58
- 2. read and update `Person` objects
59
- 3. export as `.ged` or `.zip`
124
+ ```ts
125
+ import { ReadGed } from "gedcom-ts";
60
126
 
61
- ## Export
127
+ function describe(readGed: ReadGed) {
128
+ return {
129
+ version: readGed.datasetVersion,
130
+ persons: readGed.persons.length,
131
+ places: readGed.placesMap.size,
132
+ };
133
+ }
134
+ ```
62
135
 
63
- ### GED (`.ged`)
136
+ ### Exporting
137
+
138
+ #### `ExportGedcomFile`
139
+
140
+ Writes a GEDCOM 7 (`.ged`) file. Three call signatures are supported:
64
141
 
65
142
  ```ts
66
- import { ExportGedcomFile, Person } from "gedcom-ts";
143
+ new ExportGedcomFile(persons);
144
+ new ExportGedcomFile(title, persons);
145
+ new ExportGedcomFile(title, persons, options);
146
+ ```
67
147
 
68
- function exportGed(persons: Person[]) {
69
- new ExportGedcomFile("my-tree", persons).download();
148
+ `.download()` triggers a browser download. `.toString()` returns the GEDCOM text.
149
+
150
+ ```ts
151
+ import { ExportGedcomFile, type ReadGed } from "gedcom-ts";
152
+
153
+ function exportGed(readGed: ReadGed) {
154
+ new ExportGedcomFile("my-tree", readGed.persons, {
155
+ extraTopLevelRecords: readGed.preservedTopLevelRecords,
156
+ headLanguageTag: "fr-FR",
157
+ headCopyright: "© 2026 Family Archive",
158
+ headDestination: "https://gedcom.io/",
159
+ headSchemaTagDefs: [{ tag: "_FOO", uri: "https://example.com/foo" }],
160
+ }).download();
70
161
  }
71
162
  ```
72
163
 
73
- ### ZIP (`.zip`) GED + media
164
+ #### `ExportGedzipFile`
165
+
166
+ Writes a `.zip` (GEDZIP) bundling the GEDCOM and all attached `MultimediaFile` payloads.
74
167
 
75
168
  ```ts
76
169
  import { ExportGedzipFile, Person } from "gedcom-ts";
@@ -80,37 +173,132 @@ async function exportZip(persons: Person[]) {
80
173
  }
81
174
  ```
82
175
 
83
- ## Public API with Examples
176
+ #### `GedcomExportOptions`
177
+
178
+ Options shared by both exporters:
179
+
180
+ | Option | Effect |
181
+ | --- | --- |
182
+ | `extraTopLevelRecords` | Raw `0 …` blocks re-emitted before `SUBM` / `TRLR` (round-trip with `readGed.preservedTopLevelRecords`). |
183
+ | `headLanguageTag` | BCP 47 tag for `HEAD`.`LANG` (defaults to `en-US`). |
184
+ | `headCopyright` | One-line `1 COPR` notice. |
185
+ | `headDestination` | Value of `HEAD`.`DEST` (target app / URI). |
186
+ | `headSchemaTagDefs` | Extension tag definitions emitted as `HEAD`.`SCHMA` / `2 TAG …`. |
84
187
 
85
- Everything exported from `src/index.ts` is shown below with a minimal usage snippet.
188
+ ### Domain model
86
189
 
87
- ### Person, Sex
190
+ #### `Person`, `Sex`
88
191
 
89
192
  ```ts
90
193
  import { Person, Sex } from "gedcom-ts";
91
194
 
92
195
  const person = new Person();
93
196
  person.INDI = 1;
94
- person.SEX = Sex.M;
197
+ person.SEX = Sex.M; // M | F | U | X
95
198
  person.firstnames = ["Jean"];
96
199
  person.lastname = "DUPONT";
200
+
201
+ person.addMultimedia(/* MultimediaFile */);
202
+ person.deleteMultimedia("1/photo.jpg");
203
+ ```
204
+
205
+ Key fields: `INDI`, `sosa`, `SEX`, `firstnames`, `lastname`, `FAMC`, `FAMS`, `acts`, `notes`, `multimediaFiles`, `nameVariants`, `attributes`.
206
+
207
+ #### `PersonGedcomImportOptions` (type)
208
+
209
+ Optional hints consumed by `Person.createPersonJson` when re-parsing a single individual block (label-keyed pointers for `FAMS` / `FAMC` / `NOTE`, and standalone `OBJE` payloads). Useful when assembling a graph manually outside `ReadGed`.
210
+
211
+ #### `PersonNameVariant`, `PersonNameTranslation`
212
+
213
+ Lossless representation of every `1 NAME` block of an individual (type, `NPFX`/`GIVN`/`SURN`/… parts, `TRAN` translations). `Person.nameVariants` keeps them in order so alias / translation data survive an import → export round-trip.
214
+
215
+ ```ts
216
+ import { PersonNameVariant, PersonNameTranslation } from "gedcom-ts";
217
+
218
+ const variant = new PersonNameVariant("Jean /Dupont/", "BIRTH");
219
+ variant.parts.push({ level: "2", tag: "GIVN", value: "Jean" });
220
+ variant.translations.push(new PersonNameTranslation("ジャン /デュポン/", "jp"));
221
+ ```
222
+
223
+ #### `IndiAttribute`, `IndiGedcomSubLine`
224
+
225
+ Generic level-1 individual attributes (`FACT`, `DSCR`, `CAST`, `EDUC`, `OCCU`, `RELI`, `TITL`, `RESN`, …) with their sub-lines preserved (`IndiGedcomSubLine = { level; tag; value }`).
226
+
227
+ ```ts
228
+ import { IndiAttribute } from "gedcom-ts";
229
+
230
+ const occ = new IndiAttribute("OCCU", "Blacksmith", [
231
+ { level: "2", tag: "DATE", value: "1820" },
232
+ ]);
97
233
  ```
98
234
 
99
- ### Act, Acts, TypeAct
235
+ #### `Act`, `Acts`, `TypeAct`, `ActConstructionOptions`
236
+
237
+ `Act` models an individual or family event (BIRT, MARR, etc.). `Acts` is the ordered collection on a `Person`. `TypeAct` is the union of every supported GEDCOM 7 event tag (= `Gedcom7EventTag`). Event-level notes live on `act.notes` (`2 NOTE` / `3 CONT` in GEDCOM); they are distinct from `person.notes` (`1 NOTE` on `INDI`) and round-trip through ZIP export/import.
100
238
 
101
239
  ```ts
102
- import { Act, Acts, TypeAct, Identifier } from "gedcom-ts";
240
+ import { Act, Acts, Identifier, type TypeAct } from "gedcom-ts";
103
241
 
104
- const actType: TypeAct = Identifier.BIRT;
105
- const act = new Act(actType);
106
242
  const acts = new Acts();
107
- acts.add(act);
243
+ const type: TypeAct = Identifier.BIRT;
244
+ acts.add(new Act(type));
245
+ acts.sortByDate();
246
+ ```
247
+
248
+ `ActConstructionOptions` controls the extras when building an `Act` manually:
249
+
250
+ ```ts
251
+ import { Act, DateAct, Identifier } from "gedcom-ts";
252
+
253
+ const act = new Act(
254
+ Identifier.EVEN,
255
+ new DateAct("12 JAN 1901"),
256
+ null,
257
+ null,
258
+ null,
259
+ undefined,
260
+ undefined,
261
+ {
262
+ evenDescription: "Won a medal",
263
+ evenTypeLabel: "Award",
264
+ sdateAct: new DateAct("13 JAN 1901"),
265
+ eventPhrases: ["family gathering"],
266
+ preservedSubrecordPrefix: [],
267
+ preservedSubrecordSuffix: [],
268
+ },
269
+ );
270
+ ```
271
+
272
+ #### `GEDCOM_7_ALL_EVENT_TAGS`, `GEDCOM_7_EVENT_SORT_ORDER`, `GEDCOM_7_EVENT_TAG_SET`, `GEDCOM_7_PAIR_UNION_EVENT_TAGS`, `Gedcom7EventTag`, `Gedcom7PairUnionEventTag`
273
+
274
+ Canonical lists of GEDCOM 7 event tags (`INDIVIDUAL_EVENT_STRUCTURE` ∪ `FAMILY_EVENT_STRUCTURE`, LDS ordinances excluded). Use them to build UI selects or guard custom logic. `GEDCOM_7_PAIR_UNION_EVENT_TAGS` lists tags commonly used when creating a family union via `createMarriageFamily` (`ENGA`, `MARB`, `MARC`, `MARL`, `MARR`, `MARS`).
275
+
276
+ ```ts
277
+ import {
278
+ GEDCOM_7_ALL_EVENT_TAGS,
279
+ GEDCOM_7_EVENT_SORT_ORDER,
280
+ GEDCOM_7_EVENT_TAG_SET,
281
+ type Gedcom7EventTag,
282
+ } from "gedcom-ts";
283
+
284
+ const options: Gedcom7EventTag[] = [...GEDCOM_7_ALL_EVENT_TAGS];
285
+ const isEvent = GEDCOM_7_EVENT_TAG_SET.has("MARR");
286
+ const sorted = [...GEDCOM_7_EVENT_SORT_ORDER];
108
287
  ```
109
288
 
110
- ### DateAct, Day, Month, dateToDateLine, days, months
289
+ #### `DateAct`, `Day`, `Month`, `dateToDateLine`, `days`, `months`, `TypeDateActSimpleQualifier`
290
+
291
+ GEDCOM dates with full GEDCOM 7 / 5.5 support, including `INT`, `EST`, `CAL`, `BET … AND …`, `FROM … TO …`, ISO `YYYY-MM-DD`, `3 PHRASE` under `DATE`, `3 TIME`, and a verbatim fallback for unparseable payloads.
111
292
 
112
293
  ```ts
113
- import { DateAct, Day, Month, dateToDateLine, days, months } from "gedcom-ts";
294
+ import {
295
+ DateAct,
296
+ Day,
297
+ Month,
298
+ dateToDateLine,
299
+ days,
300
+ months,
301
+ } from "gedcom-ts";
114
302
 
115
303
  const day: Day = 12;
116
304
  const month: Month = months[0]; // JAN
@@ -120,38 +308,53 @@ const formattedDate = dateAct.date;
120
308
  const knownDaysCount = days.length;
121
309
  ```
122
310
 
123
- ### MultimediaFile, MultimediaFiles
311
+ `TypeDateActSimpleQualifier` is the union of single-anchor qualifiers (`BEF | ABT | AFT | INT | EST | CAL`) accepted by `DateAct.updateQualifiedDate` and by `DateActEdit.setQualified`.
312
+
313
+ #### `Place`, `CoordinateGPS`
124
314
 
125
315
  ```ts
126
- import { MultimediaFile, MultimediaFiles } from "gedcom-ts";
316
+ import { Place, CoordinateGPS } from "gedcom-ts";
127
317
 
128
- const bucket = new MultimediaFiles();
129
- bucket.relativePath = "1/BIRT";
130
- bucket.add(new MultimediaFile(new File(["img"], "birth.jpg")));
318
+ const place = new Place("Paris, FR", new CoordinateGPS(48.8566, 2.3522));
319
+ place.setFromGedcom7PlacPayload("Paris, Île-de-France, France");
320
+ const payload = place.toGedcom7PlacPayload(); // "Paris, , Île-de-France, France"
131
321
  ```
132
322
 
133
- ### Place, CoordinateGPS
323
+ `Place` understands the `City, County, State, Country` GEDCOM 7 list (1 to 4+ segments) and exposes `placPhrase` for `3 PHRASE` under `PLAC` (import + export).
324
+
325
+ #### `MultimediaFile`, `MultimediaFiles`
326
+
327
+ Wraps either a local `File`, an external URI (`sourceUri`) or an OBJE pointer (`objeXrefId`) so the same model can survive a GED / ZIP round-trip.
134
328
 
135
329
  ```ts
136
- import { Place, CoordinateGPS } from "gedcom-ts";
330
+ import { MultimediaFile, MultimediaFiles } from "gedcom-ts";
137
331
 
138
- const place = new Place("Paris, FR", new CoordinateGPS(48.8566, 2.3522));
332
+ const bucket = new MultimediaFiles();
333
+ bucket.relativePath = "1/BIRT";
334
+ bucket.add(new MultimediaFile(new File(["img"], "birth.jpg")));
335
+
336
+ const remote = new MultimediaFile(undefined, "https://example.com/p.jpg");
337
+ const isExportable = remote.hasExportablePayload();
139
338
  ```
140
339
 
141
- ### Notes, Note, TypeNote
340
+ #### `Note`, `Notes`, `TypeNote`
142
341
 
143
342
  ```ts
144
- import { Notes, Note, TypeNote, Identifier } from "gedcom-ts";
343
+ import { Notes, Note, Identifier, type TypeNote } from "gedcom-ts";
145
344
 
146
345
  const typeNote: TypeNote = Identifier.CONT;
147
346
  const note = new Note();
148
347
  note.updateType(typeNote);
149
348
  note.updateLines(["first line", "second line"]);
349
+
150
350
  const notes = new Notes();
151
351
  notes.addNote(note);
352
+ notes.removeFromIndex(0);
152
353
  ```
153
354
 
154
- ### Identifier (preferred) / Identificator (deprecated)
355
+ #### `Identifier` (and deprecated `Identificator`)
356
+
357
+ Enum of every GEDCOM tag the library refers to (`INDI`, `BIRT`, `MARR`, `DATE`, `PLAC`, …). Prefer `Identifier`; `Identificator` is kept as a deprecated alias.
155
358
 
156
359
  ```ts
157
360
  import { Identifier } from "gedcom-ts";
@@ -159,93 +362,262 @@ import { Identifier } from "gedcom-ts";
159
362
  const birthTag = Identifier.BIRT;
160
363
  ```
161
364
 
162
- ### ExportGedcomFile, ExportGedzipFile
365
+ #### `EventsByYears`, `ActsByYear`
366
+
367
+ Group acts by year, deduplicating per individual. Handy to build chronological timelines.
163
368
 
164
369
  ```ts
165
- import { ExportGedcomFile, ExportGedzipFile, Person } from "gedcom-ts";
370
+ import { EventsByYears, ActsByYear } from "gedcom-ts";
166
371
 
167
- const persons: Person[] = [];
168
- new ExportGedcomFile("tree", persons).download();
169
- await new ExportGedzipFile("tree", persons).download();
372
+ const grouped = new EventsByYears(person.acts.list);
373
+ for (const bucket of grouped.events) {
374
+ console.log(bucket.year, bucket.list.length);
375
+ }
376
+
377
+ const yearBucket = new ActsByYear(1901);
170
378
  ```
171
379
 
172
- ### ReadGed
380
+ ### Edit layer (in-place, chainable)
381
+
382
+ Wrap an existing model object to mutate it with a fluent API. Every method returns `this`, so you can chain. `.value` exposes the underlying target.
173
383
 
174
384
  ```ts
175
- import { ReadGed } from "gedcom-ts";
385
+ import { editPerson, editDateAct, DateAct } from "gedcom-ts";
176
386
 
177
- function useReadGed(readGed: ReadGed) {
178
- return {
179
- persons: readGed.persons,
180
- places: readGed.placesMap,
181
- };
182
- }
387
+ editPerson(person)
388
+ .setLastname("Dupont")
389
+ .setFirstnames(["Jean", "Marie"])
390
+ .acts()
391
+ .at(0)
392
+ .setDateAct(new DateAct("1 JAN 1900"));
393
+
394
+ editDateAct(person.acts.list[0].dateAct!).setExactDate(1900, "JAN", 1);
183
395
  ```
184
396
 
185
- ### importGedFile
397
+ | Helper / class | Purpose |
398
+ | --- | --- |
399
+ | `editPerson(person)` / `PersonEdit` | Identity, `FAMS` / `FAMC` / `removeFams*`, `nameVariants()` / `attributes()` / `multimedia()`, bulk `clear*`, entry points to `acts()` and `notes()`. |
400
+ | `editActs(acts)` / `ActsEdit` | `add`, `addNew`, `insertAt`, `insertNewAt`, `replaceAt`, `removeAt`, `removeAct`, `removeLast`, `clear`, `sortByDate`, `at`, `indexOfAct`. |
401
+ | `editAct(act)` / `ActEdit` | `setType`, `setIndis`, dates, place, EVEN fields, preserved lines, `notes()`, `multimedia()`, `clearNotes`, `clearMultimedia`. |
402
+ | `editDateAct(dateAct)` / `DateActEdit` | `clear`, `applyGedcomPayload`, `setExactDate`, `setQualified`, `setBetween`, `setFromTo`, `setTime`, `setDatePhrase` / `appendDatePhrase`, `setVerbatimPayload`. |
403
+ | `editPlace(place)` / `PlaceEdit` | `setFromGedcom7Payload`, `setCity`, `setCounty`, `setState`, `setCountry`, `setPlacPhrase` / `appendPlacPhrase` / `clearPlacPhrase`, `setCoordinates` / `clearCoordinates` / `replaceCoordinateModel`, `clearStructured`. |
404
+ | `editNotes(notes)` / `NotesEdit` | `add`, `addNew`, `insertAt`, `insertNewAt`, `replaceAt`, `removeAt`, `removeNote`, `removeLast`, `clear`, `at`, `indexOfNote`. |
186
405
 
187
- ```ts
188
- import { importGedFile } from "gedcom-ts";
406
+ ### Dataset editing: `ReadGed`, graph, clone, export options, validation
189
407
 
190
- const readGed = await importGedFile(fileInput.files![0]);
408
+ Higher-level helpers complement the per-object edit facades:
409
+
410
+ ```ts
411
+ import type { GedcomExportOptions } from "gedcom-ts";
412
+ import {
413
+ createEmptyReadGed,
414
+ createPersonStub,
415
+ DateAct,
416
+ Sex,
417
+ editReadGed,
418
+ editGedcomExportOptions,
419
+ clonePerson,
420
+ cloneAct,
421
+ createMarriageFamily,
422
+ linkChildToFamily,
423
+ removeFamilyReferencesFromDataset,
424
+ addPersonToReadGed,
425
+ validateReadGed,
426
+ Identifier,
427
+ Act,
428
+ } from "gedcom-ts";
429
+
430
+ const readGed = createEmptyReadGed();
431
+ const p = createPersonStub(readGed.generateUniqueIndi(), { sex: Sex.M, firstnames: ["Jean"], lastname: "Dupont" });
432
+ editReadGed(readGed).addPerson(p);
433
+ p.acts.add(new Act(Identifier.BIRT, new DateAct("1900")));
434
+
435
+ editReadGed(readGed).preserved().append("0 @S42@ SOUR Custom");
436
+
437
+ const opts: GedcomExportOptions = {};
438
+ editGedcomExportOptions(opts)
439
+ .setHeadCopyright("© 2026")
440
+ .setExtraTopLevelRecords([...readGed.preservedTopLevelRecords]);
441
+
442
+ const twin = clonePerson(p, readGed.generateUniqueIndi());
443
+ const birthCopy = cloneAct(p.acts.list[0]!);
444
+
445
+ // const fam = createMarriageFamily(readGed, spouseA, spouseB, { dateAct: new DateAct("1 JAN 2000") }, { eventTag: Identifier.ENGA });
446
+ // linkChildToFamily(readGed, child, fam);
447
+ // removeFamilyReferencesFromDataset(readGed, fam);
448
+
449
+ validateReadGed(readGed, {
450
+ checkMarrParticipants: true,
451
+ checkFamcWithoutSpouses: true,
452
+ checkFamsWithoutSpouses: true,
453
+ checkDuplicateFamsEntries: true,
454
+ checkAncestorCycles: true,
455
+ });
191
456
  ```
192
457
 
193
- ### createSosaMap
458
+ #### Commands (invariants before mutation)
459
+
460
+ `tryAddPersonToReadGed`, `tryRemovePersonFromReadGedByIndi`, `tryLinkChildToFamily`, `tryUnlinkChildFromFamily`, and `tryCreateMarriageFamily` return a `CommandResult` (`ok` + `issues` or `value` + optional `warnings`). `editReadGed(...).addPerson` uses these checks internally (throws on blocking errors). For UI or transactional flows, prefer the `try*` APIs and inspect `commandBlockingIssues`.
461
+
462
+ | Export | Role |
463
+ | --- | --- |
464
+ | `editReadGed(readGed)` / `ReadGedEdit` | `addPerson`, `removePersonByIndi`, `preserved()` → `PreservedTopLevelEdit` (`append`, `insertAt`, `replaceAt`, `removeAt`, `clear`) on `preservedTopLevelRecords`. Low-level helpers: `addPersonToReadGed`, `removePersonFromReadGedByIndi`. |
465
+ | `editGedcomExportOptions(opts)` / `GedcomExportOptionsEdit` | Fluent setters for `extraTopLevelRecords`, `headLanguageTag`, `headCopyright`, `headDestination`, `headSchemaTagDefs`. |
466
+ | `clonePerson(person, newIndi)` / `cloneAct(act)` / `person.clone` / `act.clone` | Deep copies for templates or undo stacks. |
467
+ | `nextFamilyId(persons)` | Next internal family id `F`. |
468
+ | `createMarriageFamily`, `linkChildToFamily`, `unlinkChildFromFamily`, `removeFamilyReferencesFromDataset` | Crée une `F` et des actes sur les deux conjoints. 5ᵉ argument : `{ eventTag }` (défaut `MARR`) pour `ENGA`, bans, contrat, `EVEN`+`CreateActInit`, etc. Voir `GEDCOM_7_PAIR_UNION_EVENT_TAGS`. |
469
+ | `validateReadGed`, `assertReadGedConsistent`, `validatePerson`, `assertPersonConsistent` | Typed `code` / `severity` (`error` \| `warn`). Options: `checkMarrParticipants`, `checkDuplicateIndis`, `checkFamcWithoutSpouses`, `checkFamsWithoutSpouses`, `checkDuplicateFamsEntries`, `checkAncestorCycles`. Assertions: `failOn` (default: `error`). |
470
+ | `tryAddPersonToReadGed`, `tryRemovePersonFromReadGedByIndi`, `tryLinkChildToFamily`, `tryUnlinkChildFromFamily`, `tryCreateMarriageFamily`, `commandBlockingIssues` | Command layer with `CommandResult` / `validate*Command` prechecks. |
471
+
472
+ ### Utilities
473
+
474
+ #### `createSosaMap(root, partnersMap)`
475
+
476
+ Computes a Sosa numbering (Ahnentafel) starting at `root` (Sosa 1) and recursively walking ancestors via `partnersMap`.
194
477
 
195
478
  ```ts
196
479
  import { createSosaMap, Person } from "gedcom-ts";
197
480
 
198
- const root = new Person();
199
- root.INDI = 1;
200
- const sosaMap = createSosaMap(root, new Map());
481
+ const sosaMap = createSosaMap(root, readGed.partnersMap);
482
+ // e.g. sosaMap.get(root.INDI) === 1
201
483
  ```
202
484
 
203
- ### remainingTypesAct
485
+ #### `remainingTypesAct(acts, actToUpdate?)`
486
+
487
+ Returns the list of event types still allowed for a person’s `Acts`, enforcing the “unique per individual” rule for `BIRT` / `DEAT` / `BURI` / `CHR` while keeping every other tag selectable. Pass the currently edited act as `actToUpdate` so its own type stays in the list when re-opening a form.
204
488
 
205
489
  ```ts
206
490
  import { remainingTypesAct, Acts } from "gedcom-ts";
207
491
 
208
- const availableTypes = remainingTypesAct(new Acts());
492
+ const available = remainingTypesAct(new Acts());
209
493
  ```
210
494
 
211
- ### EventsByYears, ActsByYear
495
+ #### `getCityCoordinates(cityName, callback)`
496
+
497
+ Queries OpenStreetMap’s Nominatim API and returns a list of candidate `Place` objects with coordinates. Browser-only (uses `XMLHttpRequest`).
212
498
 
213
499
  ```ts
214
- import { EventsByYears, ActsByYear } from "gedcom-ts";
500
+ import { getCityCoordinates } from "gedcom-ts";
215
501
 
216
- const grouped = new EventsByYears([]);
217
- const yearBucket = new ActsByYear(1901);
218
- const eventsByYear = grouped.events;
219
- const selectedYear = yearBucket.year;
502
+ getCityCoordinates("Paris", (places) => {
503
+ console.log(places[0]?.coordinate.latitude, places[0]?.coordinate.longitude);
504
+ });
220
505
  ```
221
506
 
222
- ## End-to-End Example
507
+ #### `resolveDatasetVersion(headerLines)` / `GedcomDatasetVersion`
508
+
509
+ Inspects the lines of a `0 HEAD` block and returns `"7.0"`, `"5.5"` or `"unknown"`. Useful to branch UI behaviour for legacy datasets.
223
510
 
224
511
  ```ts
225
- import { importGedFile, ExportGedzipFile } from "gedcom-ts";
512
+ import { resolveDatasetVersion } from "gedcom-ts";
513
+
514
+ const version = resolveDatasetVersion(headLines); // "7.0" | "5.5" | "unknown"
515
+ ```
516
+
517
+ #### `guessMediaFormFromUri(uri)`
518
+
519
+ Best-effort media type detection from a path/URI (jpg, png, gif, webp, mp3, mp4, pdf, …). Used internally to fill `OBJE`.`FILE`.`FORM`.
520
+
521
+ ```ts
522
+ import { guessMediaFormFromUri } from "gedcom-ts";
523
+
524
+ guessMediaFormFromUri("https://example.com/photo.jpg"); // "image/jpeg"
525
+ ```
526
+
527
+ #### `extractPersonNameVariants(personLines)` / `extractIndiAttributes(personLines)`
528
+
529
+ Low-level parsers used by `Person.createPersonJson`. They turn the raw GEDCOM lines of a single `INDI` record into structured `PersonNameVariant[]` / `IndiAttribute[]`. Reuse them when parsing custom GEDCOM fragments outside `ReadGed`.
530
+
531
+ ```ts
532
+ import { extractPersonNameVariants, extractIndiAttributes } from "gedcom-ts";
533
+
534
+ const variants = extractPersonNameVariants(rawIndiLines);
535
+ const attributes = extractIndiAttributes(rawIndiLines);
536
+ ```
537
+
538
+ #### `selectPrimaryNameVariant(variants)`
539
+
540
+ Picks the most relevant `1 NAME` block: priority to `2 TYPE BIRTH`, then to a name with a `/surname/` payload, otherwise the first variant.
541
+
542
+ ```ts
543
+ import { selectPrimaryNameVariant } from "gedcom-ts";
544
+
545
+ const primary = selectPrimaryNameVariant(person.nameVariants);
546
+ ```
547
+
548
+ #### `GEDCOM_LIBRARY_VERSION`
549
+
550
+ CalVer string embedded in the exported `HEAD`.`SOUR`.`VERS` (same value as `package.json` `version`, e.g. `2026.5.0`). A test (`version-package-sync`) enforces the match on every CI run.
551
+
552
+ ```ts
553
+ import { GEDCOM_LIBRARY_VERSION } from "gedcom-ts";
554
+
555
+ console.log(`gedcom-ts ${GEDCOM_LIBRARY_VERSION}`);
556
+ ```
557
+
558
+ ## End-to-end example
559
+
560
+ ```ts
561
+ import {
562
+ importGedFile,
563
+ ExportGedzipFile,
564
+ editPerson,
565
+ DateAct,
566
+ Identifier,
567
+ } from "gedcom-ts";
226
568
 
227
569
  async function importModifyExport(file: File) {
228
570
  const readGed = await importGedFile(file);
229
571
  const persons = readGed.persons;
230
572
 
231
573
  if (persons.length > 0) {
232
- persons[0].lastname = persons[0].lastname.toUpperCase();
574
+ editPerson(persons[0])
575
+ .setLastname(persons[0].lastname.toUpperCase())
576
+ .acts()
577
+ .at(0)
578
+ .setDateAct(new DateAct("1 JAN 1900"));
233
579
  }
234
580
 
235
581
  await new ExportGedzipFile("updated-tree", persons).download();
236
582
  }
237
583
  ```
238
584
 
239
- ## Error Handling
585
+ ## Development
586
+
587
+ ### Local scripts
588
+
589
+ | Script | Role |
590
+ | --- | --- |
591
+ | `npm run lint` | ESLint on the codebase |
592
+ | `npm run test` | Vitest test suite |
593
+ | `npm run build` | Production bundle + `.d.ts` |
594
+ | `npm run tgz` | `build` then `npm pack` (local `.tgz`) |
595
+
596
+ ### GitLab CI (`.gitlab-ci.yml`)
597
+
598
+ Every pipeline runs **`InstallDependencies`** (`npm ci`, `node_modules` artifact) — required by all other jobs.
599
+
600
+ | Job | When it runs |
601
+ | --- | --- |
602
+ | `InstallDependencies` | Always |
603
+ | `Lint`, `Test` | Merge requests and branch pushes (not on `master` after a merge commit titled `Merge…`) |
604
+ | `BuildTgz`, `PublishNpm` | Push to `master` after merge only (`Merge…` commit title) |
605
+
606
+ Typical flow: open an MR from a release branch (e.g. `2026.5.0`) → lint + test run there → after merge to `master`, only package build and npm publish run (quality checks are not repeated).
607
+
608
+ ## Error handling
240
609
 
241
610
  ```ts
242
- import { importGedFile } from "gedcom-ts";
611
+ import { importGedFile, IMPORT_ERR_ZIP_GED_UNREADABLE } from "gedcom-ts";
243
612
 
244
613
  try {
245
614
  const readGed = await importGedFile(file);
246
- const personsCount = readGed.persons.length;
615
+ console.log(readGed.persons.length);
247
616
  } catch (error) {
248
- console.error("GED import failed:", error);
617
+ if (error instanceof Error && error.message === IMPORT_ERR_ZIP_GED_UNREADABLE) {
618
+ console.error("Encrypted ZIP: please extract the .ged manually.");
619
+ } else {
620
+ console.error("GED import failed:", error);
621
+ }
249
622
  }
250
623
  ```
251
-