UNPKG

gedcom-ts

Version:

TypeScript GEDCOM toolkit for browser apps: import .ged/.zip, edit typed genealogy data, and export GEDCOM or GEDZIP.

964 lines (716 loc) 49.6 kB
# gedcom-ts `gedcom-ts` is a browser-oriented TypeScript library to: - import genealogy data from GEDCOM (`.ged`) or ZIP (`.zip` / `.gdz`) - work with a typed JSON model (persons, acts, dates, places, notes, media, name variants, attributes) - edit the model in-place through a fluent, chainable API (`editPerson`, `editAct`, …) - export data back to GEDCOM (`.ged`) or GEDZIP (`.zip`) - attach **audio** and other media via GEDCOM 7 `OBJE` records (`MEDI AUDIO`, multi-`FILE`, `TRAN`) - model **oral history interviews** as `EVEN` acts with `OBJE` + transcription notes - geocode event places (OpenStreetMap / Nominatim) and group similar city names - generate a **genealogy booklet** as PDF (`gedcom-ts/booklet`, 11 locales including RTL Arabic/Hebrew) ## Live demo A graphical demo showcasing the public API (import a `.ged` / `.zip`, browse the typed model, export back) is available at **[https://gedcomts.com](https://gedcomts.com)** ## Contents - [Installation](#installation) - [Quick start](#quick-start) - [Geocoding places](#geocoding-places) - [Genealogy booklet (PDF)](#genealogy-booklet-pdf) - [Audio and oral history (GEDCOM 7)](#audio-and-oral-history-gedcom-7) - [API reference](#api-reference) - [Error handling](#error-handling) ## Package - NPM: [gedcom-ts](https://www.npmjs.com/package/gedcom-ts) - Current release **`2026.6.4`** — affichage des prénoms GEDCOM 7 sans guillemets parasites après round-trip ; série **2026.6.x** (OBJE audio, GEDZIP fflate, dates insensibles à la casse). Version format **CalVer** `AAAA.M.micro` (e.g. `2026.6.4` = June 2026, micro release). See [CHANGELOG.md](CHANGELOG.md). - Entry points: - **`gedcom-ts`** — import, model, edit layer, export, geocoding - **`gedcom-ts/booklet`** — PDF livret (`pdf-lib` en peerDependency, chargé depuis `node_modules`) ## Installation ```bash npm install gedcom-ts ``` Both entry points come from the same package. For the PDF booklet, **`pdf-lib` is a peer dependency** — install it in your app if you use `gedcom-ts/booklet`: ```bash npm install pdf-lib ``` For `zh` / `ar` / `he` PDF output, your bundler should also be able to resolve `@pdf-lib/fontkit` (included in `gedcom-ts` dependencies; the host app typically preloads it — see [Bundle layout](#bundle-layout) below). ## Runtime Requirements - modern browser runtime (`File`, `Blob`, `XMLHttpRequest`, `URL.createObjectURL`) - for pure Node.js usage, DOM polyfills are required (the library targets browsers) ## Quick start ```ts import { importGedFile, ExportGedzipFile } from "gedcom-ts"; async function roundTrip(file: File) { const readGed = await importGedFile(file); const persons = readGed.persons; if (persons.length > 0) { persons[0].lastname = persons[0].lastname.toUpperCase(); } await new ExportGedzipFile("updated-tree", persons).download(); } ``` Typical workflow: 1. import a file with `importGedFile` (or start from `createEmptyReadGed()`) 2. read / mutate the typed `Person`, `Act`, `Place`, `Note`, `MultimediaFile` objects (directly or through `editPerson` / `editAct` / `editPlace` / …) 3. export as `.ged` (`ExportGedcomFile`) or `.zip` (`ExportGedzipFile`) ## Geocoding places Use this section when your app needs **GPS coordinates** on event places, a **list of cities still without coordinates**, **map markers**, or a **data-quality** view for inconsistent place names. You work with a `ReadGed` (after import) and a flat `Act[]` built from every person’s events (see [Prepare act list](#prepare-act-list)). ### How city names are matched The library groups variants of the same place with **`citiesAreSimilar`**, not exact string equality. | Label A | Label B | Same place? | | ----------- | ------------------------------------ | --------------------------- | | `Pleurtuit` | `Pleurtuit, Ille-et-Vilaine, France` | Yes | | `Paris` | `Paris, TX, USA` | Depends on similarity rules | Use the functions in the tables below for grouping, geocoding, and maps. Do **not** compare raw strings yourself, and do **not** use `normalizeCityKey` for grouping (spelling normalization only). ### Prepare act list ```ts import type { ReadGed, Act } from "gedcom-ts"; function collectAllActs(ged: ReadGed): Act[] { const acts: Act[] = []; for (const person of ged.persons) { for (const act of person.acts.list) acts.push(act); } return acts; } ``` ### Geocode one city (typical flow) When the user picks a city and a Nominatim result, update **every similar act that still has no GPS** — not only acts with the exact same label. | Step | Function | | ---------------------------------------------- | ----------------------------------------------------------- | | 1. Context from the tree (countries, centroid) | `inferGeocodeContext(ged)` | | 2. Search OpenStreetMap | `searchPlacesWithContext(cityName, context)` | | 3. Acts to update | `actsNeedingGeocodeForCity(allActs, cityName)` | | 4. Write coordinates | `applyGeocodeCandidateToActs(targets, candidate, cityName)` | ```ts import { inferGeocodeContext, searchPlacesWithContext, actsNeedingGeocodeForCity, applyGeocodeCandidateToActs, } from "gedcom-ts"; import type { ReadGed, Act } from "gedcom-ts"; async function geocodeCity(ged: ReadGed, allActs: Act[], cityName: string) { const context = inferGeocodeContext(ged); const candidates = await searchPlacesWithContext(cityName, context); const chosen = candidates[0]; if (!chosen) return; const targets = actsNeedingGeocodeForCity(allActs, cityName); applyGeocodeCandidateToActs(targets, chosen, cityName); } ``` > **Tip:** Do not pass only acts from a harmonization cluster to `applyGeocodeCandidateToActs`. Longer labels (e.g. `Pleurtuit, Ille-et-Vilaine, France`) would stay without GPS. Always use `actsNeedingGeocodeForCity`. ### List cities missing coordinates One row per city; `withoutCoordCount` is how many acts still need GPS. ```ts import { groupActsBySimilarCity } from "gedcom-ts"; const groups = groupActsBySimilarCity(allActs, { onlyWithoutCoordinates: true, }); for (const group of groups) { console.log(group.cityLabel, group.withoutCoordCount); // group.acts — acts in this group without GPS } ``` Largest groups first (`group.acts.length`). ### Map: one marker per city ```ts import { clusterKeyForCity } from "gedcom-ts"; const markerKey = clusterKeyForCity(act.place?.city ?? ""); // merge markers that share the same markerKey ``` ### Harmonization (data quality, optional) Use when spellings, GPS positions, or “some acts with / without GPS” disagree for the same similar city. ```ts import { findHarmonizationClusters } from "gedcom-ts"; for (const cluster of findHarmonizationClusters(ged)) { console.log(cluster.labels, cluster.coordVariants, cluster.actsWithoutCoord); } ``` After a full geocode via `actsNeedingGeocodeForCity`, you should not get a cluster that only reports missing GPS for that city. ### Geocoding API cheat sheet | Goal | Call | | ------------------------- | ---------------------------------------------------------------- | | Search | `searchPlacesWithContext(city, inferGeocodeContext(ged))` | | Acts to update on confirm | `actsNeedingGeocodeForCity(acts, city)` | | Apply lat/lng | `applyGeocodeCandidateToActs(targets, candidate, city)` | | Cities without GPS | `groupActsBySimilarCity(acts, { onlyWithoutCoordinates: true })` | | Map marker id | `clusterKeyForCity(city)` | | Inconsistencies | `findHarmonizationClusters(ged)` | Network: `GET https://nominatim.openstreetmap.org/search?q=…&format=jsonv2` with a `User-Agent` (`gedcom-ts/<version> (genealogy library)`). For offline or mocked search, pass `fetchFn` in `searchPlaces` / `searchPlacesWithContext` options. The legacy callback `getCityCoordinates` is deprecated — use the flow above. ## Genealogy booklet (PDF) The **`gedcom-ts/booklet`** subpath builds a printable family booklet: cover page, table of contents, chapters by generation (Sosa), family sheets, and narrative text from GEDCOM acts (birth, marriage, death, …). **English is the default locale** (`DEFAULT_BOOKLET_LOCALE`). Full copy is available for **`en`**, **`fr`**, **`de`**, **`nl`**, **`es`**, **`zh`**, **`it`**, **`pt`**, **`pl`**, **`ar`**, and **`he`** (aligned with [gedcomts.com](https://gedcomts.com) UI languages). Arabic and Hebrew use **RTL layout** in the PDF (`isBookletLocaleRtl`, `bookletTextDirection`). ### Bundle layout Code is split so the host app only downloads what it uses. Approximate sizes after minify (obfuscation adds ~2530 KiB on `gedcom-ts/booklet`). | Artifact | Size (typ.) | When loaded | | ------------------------------------------- | ----------: | -------------------------------------------- | | `gedcom-ts` (`dist/index.mjs`) | ~180 KiB | GEDCOM import / edit / export | | `gedcom-ts/booklet` (`dist/booklet.mjs`) | ~100 KiB | livret (collect, estimate, PDF) | | `locale-chunks/booklet-locale-*` | 1426 KiB | one UI language | | `feature-chunks/booklet-logo-draw` | ~11 KiB | cover logo (`coverLogo !== false`) | | `feature-chunks/booklet-timeline-raster` | ~3 KiB | `timelineStyle: "canvas"` | | `font-chunks/booklet-pdf-font-bytes-ar` | ~43 KiB | Arabic PDF | | `font-chunks/booklet-pdf-font-bytes-he` | ~13 KiB | Hebrew PDF | | `font-chunks/booklet-pdf-font-bytes-zh` | ~190 KiB | Chinese PDF (base subset) | | `font-chunks/booklet-pdf-font-bytes-zh-ext` | ~1.4 MiB | Chinese PDF when GEDCOM names need extra Han | | `pdf-lib` (peer, not in gedcom-ts) | ~500 KiB+ | any PDF generation | **Not bundled in gedcom-ts:** `fflate` (main entry, external), `pdf-lib` (booklet peer). **`bidi-js`** and **`naqqash`** (Arabic shaping / RTL reordering) ship inside `gedcom-ts/booklet` today. Call **`await ensureBookletLocale(locale)`** before sync APIs. `generateGenealogyBookletPdf` preloads locale, logo, and timeline when needed. **Lazy chunk subpaths** (for bundler preload or explicit `import()` — normal apps should use `ensureBookletLocale()` instead): | Subpath | Role | | ----------------------------------------------------------------------------------- | --------------------------------------------------- | | `gedcom-ts/booklet/locale-chunks/booklet-locale-{en,fr,de,nl,es,zh,it,pt,pl,ar,he}` | Messages, narratives, date/place helpers per locale | | `gedcom-ts/booklet/font-chunks/booklet-pdf-font-bytes-{ar,he,zh,zh-ext}` | Subset Noto font bytes | | `gedcom-ts/booklet/feature-chunks/booklet-logo-draw` | Cover logo SVG paths | | `gedcom-ts/booklet/feature-chunks/booklet-timeline-raster` | Canvas timeline PNG rasterizer | Each subpath is listed in `package.json` **`exports`** with matching **`types`** (`.d.ts` under `dist/booklet/…`) and runtime (`.mjs` / `.cjs` under `dist/…`). TypeScript resolves them via `exports.types` (`moduleResolution: bundler` / `node16`). Adding a locale updates `scripts/booklet-chunk-manifest.mjs`; `npm run build:js` regenerates chunk files and syncs `exports`. ### Vite / Angular integration 1. **Lazy-load the booklet entry** — `import('gedcom-ts/booklet')` only when the user opens export / download (not on every tree page). 2. **Preload the UI locale** — `import('gedcom-ts/booklet/locale-chunks/booklet-locale-fr')` (or your loader) before sync APIs; then `ensureBookletLocale('fr')`. 3. **Unicode PDF (`zh` / `ar` / `he`)** — the host app should import font chunks explicitly and call `registerBookletPdfFontBytes` + `registerPdfFontkit` before `generateGenealogyBookletPdf` (see [gedcomts.com](https://gedcomts.com) `booklet-pdf-deps.loader.ts` for a reference pattern). 4. **Dev server** — exclude the main booklet bundle from Vite pre-bundling so lazy subpaths resolve via `node_modules`: ```ts // vite.config.ts / angular.json (esbuild optimizeDeps) optimizeDeps: { exclude: ["gedcom-ts/booklet"], }, ``` Published `dist/booklet.mjs` keeps **literal** dynamic `import('gedcom-ts/booklet/…')` strings (rewrite before obfuscation, paths reserved from string encoding) so Vite can statically analyze chunk loads. ### Workflow 1. Import with `importGedFile` (`gedcom-ts`). 2. **`await ensureBookletLocale(locale)`** — load messages/narratives for the chosen language. 3. Collect persons with `collectBookletPersons` (`gedcom-ts/booklet`). 4. Optionally preview size with `estimateBookletSize` + `groupBookletIntoChapters`. 5. Build bytes with `generateGenealogyBookletPdf`, then `downloadBookletPdf` (browser). ```ts import { importGedFile } from "gedcom-ts"; import { collectBookletPersons, ensureBookletLocale, groupBookletIntoChapters, estimateBookletSize, generateGenealogyBookletPdf, downloadBookletPdf, personDisplayName, } from "gedcom-ts/booklet"; async function exportBooklet(file: File) { const ged = await importGedFile(file); const root = ged.persons[0] ?? null; const locale = "en" as const; // BookletLocale — align with your UI language (en, fr, de, nl, es, zh, it, pt, pl, ar, he) await ensureBookletLocale(locale); const entries = collectBookletPersons({ ged, scope: "from-reference", referencePerson: root, maxGeneration: 6, locale, }); const chapters = groupBookletIntoChapters(entries, locale); const families = chapters.reduce((n, ch) => n + ch.families.length, 0); const size = estimateBookletSize( chapters, entries.length, families, "summary", "canvas", locale, ); console.log(size.label); const pdf = await generateGenealogyBookletPdf( entries, { title: "Family booklet", scopeLabel: "Ancestors and descendants", personCount: entries.length, referenceName: root ? personDisplayName(root) : undefined, }, { detailLevel: "summary", timelineStyle: "canvas", coverLogo: true, locale, }, ); downloadBookletPdf(pdf, "family-booklet.pdf"); } ``` Use `scope: "all"` and `referencePerson: null` to include every individual in the file. ### `collectBookletPersons` options | Option | Role | | ----------------- | --------------------------------------------------------------------------------------------------- | | `ged` | `ReadGed` after import | | `scope` | `"all"` or `"from-reference"` (Sosa from `referencePerson`) | | `referencePerson` | Root person for `"from-reference"`; `null` if scope is `"all"` | | `maxGeneration` | Max Sosa generation (e.g. `6`); ignored when `scope === "all"` | | `locale` | `BookletLocale` (default `"en"`) — `en`, `fr`, `de`, `nl`, `es`, `zh`, `it`, `pt`, `pl`, `ar`, `he` | Helpers: `personDisplayName`, `buildBookletPersonEntry`, `sortBookletEntries`, `bookletSexFromPerson`, `DEFAULT_BOOKLET_LOCALE`, `BOOKLET_LOCALES`, `isBookletLocaleRtl`. ### `generateGenealogyBookletPdf` options | Option | Values | Effect | | --------------- | ------------------------------------------------------- | ------------------------------------------------------ | | `detailLevel` | `"summary"` \| `"detailed"` | Short prose per person vs longer biographies | | `timelineStyle` | `"off"` \| `"canvas"` | Generation timeline pages per chapter | | `coverLogo` | `true` (default), `false`, or `DrawGedcomTsLogoOptions` | gedcom-ts vector logo on the cover | | `locale` | `BookletLocale` (default `"en"`) | Narrative, chapter titles, PDF chrome, timeline legend | | `unicodeFont` | `Uint8Array` (optional) | Replace bundled Noto font for `zh` / `ar` / `he` | `BookletPdfMeta`: `title`, `scopeLabel`, `personCount`, optional `referenceName` (usually translated in the host app). ### Cover logo The cover draws the **gedcom-ts logo** from lazy-loaded SVG paths (`ensureBookletLogo`, `getGedcomTsLogoPaths`, `getGedcomTsLogoViewbox`). Reuse on custom PDF pages: ```ts import { PDFDocument } from "pdf-lib"; import { drawGedcomTsLogoOnPage, defaultCoverLogoOptions, ensureBookletLogo, } from "gedcom-ts/booklet"; const doc = await PDFDocument.create(); const page = doc.addPage(); await ensureBookletLogo(); await drawGedcomTsLogoOnPage(page, defaultCoverLogoOptions()); ``` ### Booklet API cheat sheet | Goal | Export | | -------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | Persons for the booklet | `collectBookletPersons` | | Chapters / families | `groupBookletIntoChapters`, `partnerNamesLabel` | | Localized narratives (custom UI) | `buildPersonSummaryNarrative`, `buildPersonDetailedNarratives`, `buildFamilyNarrative`, `buildChapterIntroNarrative` | | Page estimate | `estimateBookletSize`, `bookletSizeAdvice` | | Timeline data / PNG | `buildGenerationTimeline`, `rasterizeGenerationTimelinePng` | | PDF output | `generateGenealogyBookletPdf`, `downloadBookletPdf`, `toPdfText`, `preparePdfText` | | Locale helpers | `resolveBookletLocale`, `bookletLocaleToBcp47`, `bookletTextDirection`, `BOOKLET_RTL_LOCALES`, `ensureBookletLocale` | | Font preload (host) | `registerBookletPdfFontBytes`, `registerPdfFontkit`, `localeNeedsUnicodePdfFont` | | Logo | `ensureBookletLogo`, `drawGedcomTsLogoOnPage`, `getGedcomTsLogoPaths`, `defaultCoverLogoOptions` | ## Audio and oral history (GEDCOM 7) Since **`2026.6.0`**, gedcom-ts models audio according to [FamilySearch GEDCOM 7](https://gedcom.io/): external files referenced by **`OBJE`** records (`FILE` + `FORM` + optional `MEDI AUDIO` + `TRAN` for alternate formats or WebVTT). Text transcriptions belong in **`NOTE`** under the event, not inside `OBJE`. **Oral history pattern:** `1 EVEN` + `2 TYPE Interview` (or `Oral history`) + `2 OBJE @O…@` + `2 NOTE` (transcription). ```ts import { createAudioInterviewAct, editAct, ExportGedzipFile, type Person, type ReadGed, } from "gedcom-ts"; async function addInterview(readGed: ReadGed, person: Person, mp3: File) { const objeMap = new Map(readGed.objeRecordsById); const { act, objeRecord } = createAudioInterviewAct( { description: "Entretien avec grand-mère", participantIndis: [person.INDI], ownerIndi: person.INDI, audioFile: mp3, transcription: "Bonjour, je m'appelle Marie…", vttUri: "media/transcript.vtt", // optional FILE.TRAN on OBJE }, readGed.persons, objeMap, ); person.acts.add(act); if (objeRecord) { objeMap.set(objeRecord.id, objeRecord); } await new ExportGedzipFile("my-tree", readGed.persons, { extraTopLevelRecords: readGed.preservedTopLevelRecords, objeRecordsById: objeMap, }).download(); } // enrich an existing act: editAct(act) .asAudioInterview(readGed.persons, objeMap) .setTranscription("Suite de l'entretien…") .attachAudio({ audioUri: "https://example.org/rec.mp3", ownerIndi: person.INDI, }); ``` | Goal | API | | -------------------------- | ------------------------------------------------------------------------ | | Create interview act | `createAudioInterviewAct` | | Attach audio to act | `attachAudioInterviewToAct`, `editAct(act).asAudioInterview()` | | Detect imported interviews | `isAudioInterviewAct`, `isAudioInterviewEvenType` | | Read transcription / media | `getAudioInterviewTranscription`, `primaryAudioInterviewUri` | | OBJE round-trip | `readGed.objeRecordsById``objeRecordsById` in export options | | Low-level OBJE parse/emit | `parseStandaloneObjeBlock`, `formatObjeRecordBlock`, `guessMediFromForm` | | EVEN type constants | `GEDCOM_7_EVEN_TYPE_AUDIO_INTERVIEW`, `GEDCOM_7_EVEN_TYPE_ORAL_HISTORY` | ## API reference Short description and a minimal snippet for each export of **`gedcom-ts`**. For the PDF booklet, see [Genealogy booklet (PDF)](#genealogy-booklet-pdf) (`gedcom-ts/booklet`). ### Importing a file #### `importGedFile(file: File): Promise<ReadGed>` Detects the format from the file MIME / extension and dispatches to the right reader. - accepts a single `.ged` file - accepts a `.zip` / `.gdz` archive containing one `.ged` + optional media files - throws `Error(IMPORT_ERR_ZIP_GED_UNREADABLE)` when the embedded `.ged` cannot be decoded (typical cause: password-protected ZIP) ```ts import { importGedFile } from "gedcom-ts"; const readGed = await importGedFile(fileInput.files![0]); console.log(readGed.persons.length, readGed.datasetVersion); ``` #### `createEmptyReadGed(options?): ReadGed` 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"`. ```ts import { createEmptyReadGed } from "gedcom-ts"; const readGed = createEmptyReadGed(); ``` #### `IMPORT_ERR_ZIP_GED_UNREADABLE: string` 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. ```ts import { importGedFile, IMPORT_ERR_ZIP_GED_UNREADABLE } from "gedcom-ts"; try { await importGedFile(file); } catch (error) { if ( error instanceof Error && error.message === IMPORT_ERR_ZIP_GED_UNREADABLE ) { alert("Please unzip the archive manually and import the .ged file."); } } ``` #### `ReadGed` Result of an import. Standalone `0 @O…@ OBJE` blocks with at least one `FILE` are parsed into **`objeRecordsById`** (multi-file, `MEDI`, `TRAN`). Other top-level records (`REPO`, `SOUR`, `SUBM`, …) stay on `preservedTopLevelRecords` for round-trip export. Notable members: | Member | Description | | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `persons: Person[]` | Imported individuals. | | `mapPersons: Map<number, Person>` | `INDI` (integer) → `Person`. | | `partnersMap: Map<number, Person[]>` | Family id → spouses. | | `childsMap: Map<number, Person[]>` | Family id → children. | | `placesMap: Map<string, Place>` | City name → `Place` (first occurrence wins). | | `mapFiles: Map<string, File>` | Relative path → media `File` (for ZIP imports). | | `datasetVersion: GedcomDatasetVersion` | `"7.0"`, `"5.5"` or `"unknown"`. | | `objeRecordsById: Map<number, ObjeRecord>` | Parsed `OBJE` records (audio, images, …) keyed by `@O{n}@`. | | `preservedTopLevelRecords: string[]` | Raw blocks for partial round-trip. | | `resolveIndividualPointer(raw)` | Resolves `@I12@`, `I12`, `@Homer_Simpson@`, `Homer_Simpson` to a `Person`. | | `getChildrenForParent(parent)` | All children attached to a parent (via `FAMS`). | | `getChildrenOfFamily(familyId)` | Children of a single family. | | `groupPartners()` | Rebuilds `partnersMap` / `childsMap` after editing links. | | `generateUniqueIndi()` | Next free `INDI` number for new persons. | | `rehydratePlacesFromActs()` | Rebuilds `placesMap` from act places (e.g. after manual graph edits). Use `editReadGed(readGed).addPerson(...)` to register persons so maps stay coherent. | ```ts import { ReadGed } from "gedcom-ts"; function describe(readGed: ReadGed) { return { version: readGed.datasetVersion, persons: readGed.persons.length, places: readGed.placesMap.size, }; } ``` ### Exporting #### `ExportGedcomFile` Writes a GEDCOM 7 (`.ged`) file. Three call signatures are supported: ```ts new ExportGedcomFile(persons); new ExportGedcomFile(title, persons); new ExportGedcomFile(title, persons, options); ``` `.download()` triggers a browser download. `.toString()` returns the GEDCOM text. ```ts import { ExportGedcomFile, type ReadGed } from "gedcom-ts"; function exportGed(readGed: ReadGed) { new ExportGedcomFile("my-tree", readGed.persons, { extraTopLevelRecords: readGed.preservedTopLevelRecords, objeRecordsById: readGed.objeRecordsById, headLanguageTag: "fr-FR", headCopyright: "© 2026 Family Archive", headDestination: "https://gedcom.io/", headSchemaTagDefs: [{ tag: "_FOO", uri: "https://example.com/foo" }], }).download(); } ``` #### `ExportGedzipFile` Writes a `.zip` (GEDZIP) bundling the GEDCOM and all attached `MultimediaFile` payloads. Built with **fflate** (`zip` async, level 9). ```ts import { ExportGedzipFile, Person } from "gedcom-ts"; async function exportZip(persons: Person[]) { await new ExportGedzipFile("my-tree", persons).download(); } async function exportZipBlob(persons: Person[]) { return new ExportGedzipFile("my-tree", persons).gedzipBlob(); } ``` #### `GedcomExportOptions` Options shared by both exporters: | Option | Effect | | ---------------------- | -------------------------------------------------------------------------------------------------------- | | `extraTopLevelRecords` | Raw `0 …` blocks re-emitted before `SUBM` / `TRLR` (round-trip with `readGed.preservedTopLevelRecords`). | | `objeRecordsById` | Full `OBJE` records (multi-`FILE`, `MEDI AUDIO`, `TRAN`) from `readGed.objeRecordsById`. | | `headLanguageTag` | BCP 47 tag for `HEAD`.`LANG` (defaults to `en-US`). | | `headCopyright` | One-line `1 COPR` notice. | | `headDestination` | Value of `HEAD`.`DEST` (target app / URI). | | `headSchemaTagDefs` | Extension tag definitions emitted as `HEAD`.`SCHMA` / `2 TAG …`. | ### Domain model #### `Person`, `Sex` ```ts import { Person, Sex } from "gedcom-ts"; const person = new Person(); person.INDI = 1; person.SEX = Sex.M; // M | F | U | X person.firstnames = ["Jean"]; person.lastname = "DUPONT"; person.addMultimedia(/* MultimediaFile */); person.deleteMultimedia("1/photo.jpg"); ``` Key fields: `INDI`, `sosa`, `SEX`, `firstnames`, `lastname`, `FAMC`, `FAMS`, `acts`, `notes`, `multimediaFiles`, `nameVariants`, `attributes`. #### `PersonGedcomImportOptions` (type) 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`. #### `PersonNameVariant`, `PersonNameTranslation` GEDCOM 7 may quote the preferred given name in `1 NAME` (e.g. `"Jean" /Dupont/`). Since **2026.6.4**, `personDisplayName`, `personInitialsFromPerson`, and `formatPersonNameFromVariant` strip those quotes for UI display after import or export round-trip. 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. ```ts import { PersonNameVariant, PersonNameTranslation } from "gedcom-ts"; const variant = new PersonNameVariant("Jean /Dupont/", "BIRTH"); variant.parts.push({ level: "2", tag: "GIVN", value: "Jean" }); variant.translations.push(new PersonNameTranslation("ジャン /デュポン/", "jp")); ``` #### `IndiAttribute`, `IndiGedcomSubLine` Generic level-1 individual attributes (`FACT`, `DSCR`, `CAST`, `EDUC`, `OCCU`, `RELI`, `TITL`, `RESN`, …) with their sub-lines preserved (`IndiGedcomSubLine = { level; tag; value }`). ```ts import { IndiAttribute } from "gedcom-ts"; const occ = new IndiAttribute("OCCU", "Blacksmith", [ { level: "2", tag: "DATE", value: "1820" }, ]); ``` #### `Act`, `Acts`, `TypeAct`, `ActConstructionOptions` `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. ```ts import { Act, Acts, Identifier, type TypeAct } from "gedcom-ts"; const acts = new Acts(); const type: TypeAct = Identifier.BIRT; acts.add(new Act(type)); acts.sortByDate(); ``` `ActConstructionOptions` controls the extras when building an `Act` manually: ```ts import { Act, DateAct, Identifier } from "gedcom-ts"; const act = new Act( Identifier.EVEN, new DateAct("12 JAN 1901"), null, null, null, undefined, undefined, { evenDescription: "Won a medal", evenTypeLabel: "Award", sdateAct: new DateAct("13 JAN 1901"), eventPhrases: ["family gathering"], preservedSubrecordPrefix: [], preservedSubrecordSuffix: [], }, ); ``` #### `GEDCOM_7_ALL_EVENT_TAGS`, `GEDCOM_7_EVENT_SORT_ORDER`, `GEDCOM_7_EVENT_TAG_SET`, `GEDCOM_7_PAIR_UNION_EVENT_TAGS`, `Gedcom7EventTag`, `Gedcom7PairUnionEventTag` 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`). ```ts import { GEDCOM_7_ALL_EVENT_TAGS, GEDCOM_7_EVENT_SORT_ORDER, GEDCOM_7_EVENT_TAG_SET, type Gedcom7EventTag, } from "gedcom-ts"; const options: Gedcom7EventTag[] = [...GEDCOM_7_ALL_EVENT_TAGS]; const isEvent = GEDCOM_7_EVENT_TAG_SET.has("MARR"); const sorted = [...GEDCOM_7_EVENT_SORT_ORDER]; ``` #### `DateAct`, `Day`, `Month`, `dateToDateLine`, `days`, `months`, `TypeDateActSimpleQualifier` 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. Month tokens (`JAN`, `may`, …) and simple qualifiers are matched **case-insensitively** (since **2026.6.2**). ```ts import { DateAct, Day, Month, dateToDateLine, days, months } from "gedcom-ts"; const day: Day = 12; const month: Month = months[0]; // JAN const dateLine = dateToDateLine(1901, month, day); // "12 JAN 1901" const dateAct = new DateAct(dateLine); const formattedDate = dateAct.date; const knownDaysCount = days.length; ``` `TypeDateActSimpleQualifier` is the union of single-anchor qualifiers (`BEF | ABT | AFT | INT | EST | CAL`) accepted by `DateAct.updateQualifiedDate` and by `DateActEdit.setQualified`. #### `Place`, `CoordinateGPS` ```ts import { Place, CoordinateGPS } from "gedcom-ts"; const place = new Place("Paris, FR", new CoordinateGPS(48.8566, 2.3522)); place.setFromGedcom7PlacPayload("Paris, Île-de-France, France"); const payload = place.toGedcom7PlacPayload(); // "Paris, , Île-de-France, France" ``` `Place` understands the `City, County, State, Country` GEDCOM 7 list (1 to 4+ segments) and exposes `placPhrase` for `3 PHRASE` under `PLAC` (import + export). #### `MultimediaFile`, `MultimediaFiles` 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. See [Audio and oral history](#audio-and-oral-history-gedcom-7) for interview workflows. ```ts import { MultimediaFile, MultimediaFiles } from "gedcom-ts"; const bucket = new MultimediaFiles(); bucket.relativePath = "1/BIRT"; bucket.add(new MultimediaFile(new File(["img"], "birth.jpg"))); const remote = new MultimediaFile(undefined, "https://example.com/p.jpg"); const isExportable = remote.hasExportablePayload(); ``` #### `Note`, `Notes`, `TypeNote` ```ts import { Notes, Note, Identifier, type TypeNote } from "gedcom-ts"; const typeNote: TypeNote = Identifier.CONT; const note = new Note(); note.updateType(typeNote); note.updateLines(["first line", "second line"]); const notes = new Notes(); notes.addNote(note); notes.removeFromIndex(0); ``` #### `Identifier` (and deprecated `Identificator`) Enum of every GEDCOM tag the library refers to (`INDI`, `BIRT`, `MARR`, `DATE`, `PLAC`, …). Prefer `Identifier`; `Identificator` is kept as a deprecated alias. ```ts import { Identifier } from "gedcom-ts"; const birthTag = Identifier.BIRT; ``` #### `EventsByYears`, `ActsByYear` Group acts by year, deduplicating per individual. Handy to build chronological timelines. ```ts import { EventsByYears, ActsByYear } from "gedcom-ts"; const grouped = new EventsByYears(person.acts.list); for (const bucket of grouped.events) { console.log(bucket.year, bucket.list.length); } const yearBucket = new ActsByYear(1901); ``` ### Edit layer (in-place, chainable) 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. ```ts import { editPerson, editDateAct, DateAct } from "gedcom-ts"; editPerson(person) .setLastname("Dupont") .setFirstnames(["Jean", "Marie"]) .acts() .at(0) .setDateAct(new DateAct("1 JAN 1900")); editDateAct(person.acts.list[0].dateAct!).setExactDate(1900, "JAN", 1); ``` | Helper / class | Purpose | | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `editPerson(person)` / `PersonEdit` | Identity, `FAMS` / `FAMC` / `removeFams*`, `nameVariants()` / `attributes()` / `multimedia()`, bulk `clear*`, entry points to `acts()` and `notes()`. | | `editActs(acts)` / `ActsEdit` | `add`, `addNew`, `insertAt`, `insertNewAt`, `replaceAt`, `removeAt`, `removeAct`, `removeLast`, `clear`, `sortByDate`, `at`, `indexOfAct`. | | `editAct(act)` / `ActEdit` | `setType`, `setIndis`, dates, place, EVEN fields, preserved lines, `notes()`, `multimedia()`, `asAudioInterview()`, `clearNotes`, `clearMultimedia`. | | `editAudioInterviewAct(act)` / `AudioInterviewActEdit` | Oral-history facade: `attachAudio`, `setTranscription`, `setDescription`, `registerPendingObje`, `isAudioInterview`, … | | `editDateAct(dateAct)` / `DateActEdit` | `clear`, `applyGedcomPayload`, `setExactDate`, `setQualified`, `setBetween`, `setFromTo`, `setTime`, `setDatePhrase` / `appendDatePhrase`, `setVerbatimPayload`. | | `editPlace(place)` / `PlaceEdit` | `setFromGedcom7Payload`, `setCity`, `setCounty`, `setState`, `setCountry`, `setPlacPhrase` / `appendPlacPhrase` / `clearPlacPhrase`, `setCoordinates` / `clearCoordinates` / `replaceCoordinateModel`, `clearStructured`. | | `editNotes(notes)` / `NotesEdit` | `add`, `addNew`, `insertAt`, `insertNewAt`, `replaceAt`, `removeAt`, `removeNote`, `removeLast`, `clear`, `at`, `indexOfNote`. | ### Dataset editing: `ReadGed`, graph, clone, export options, validation Higher-level helpers complement the per-object edit facades: ```ts import type { GedcomExportOptions } from "gedcom-ts"; import { createEmptyReadGed, createPersonStub, DateAct, Sex, editReadGed, editGedcomExportOptions, clonePerson, cloneAct, createMarriageFamily, linkChildToFamily, removeFamilyReferencesFromDataset, addPersonToReadGed, validateReadGed, Identifier, Act, } from "gedcom-ts"; const readGed = createEmptyReadGed(); const p = createPersonStub(readGed.generateUniqueIndi(), { sex: Sex.M, firstnames: ["Jean"], lastname: "Dupont", }); editReadGed(readGed).addPerson(p); p.acts.add(new Act(Identifier.BIRT, new DateAct("1900"))); editReadGed(readGed).preserved().append("0 @S42@ SOUR Custom"); const opts: GedcomExportOptions = {}; editGedcomExportOptions(opts) .setHeadCopyright("© 2026") .setExtraTopLevelRecords([...readGed.preservedTopLevelRecords]); const twin = clonePerson(p, readGed.generateUniqueIndi()); const birthCopy = cloneAct(p.acts.list[0]!); // const fam = createMarriageFamily(readGed, spouseA, spouseB, { dateAct: new DateAct("1 JAN 2000") }, { eventTag: Identifier.ENGA }); // linkChildToFamily(readGed, child, fam); // removeFamilyReferencesFromDataset(readGed, fam); validateReadGed(readGed, { checkMarrParticipants: true, checkFamcWithoutSpouses: true, checkFamsWithoutSpouses: true, checkDuplicateFamsEntries: true, checkAncestorCycles: true, }); ``` #### Commands (invariants before mutation) `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`. | Export | Role | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `editReadGed(readGed)` / `ReadGedEdit` | `addPerson`, `removePersonByIndi`, `preserved()` → `PreservedTopLevelEdit` (`append`, `insertAt`, `replaceAt`, `removeAt`, `clear`) on `preservedTopLevelRecords`. Low-level helpers: `addPersonToReadGed`, `removePersonFromReadGedByIndi`. | | `editGedcomExportOptions(opts)` / `GedcomExportOptionsEdit` | Fluent setters for `extraTopLevelRecords`, `objeRecordsById`, `headLanguageTag`, `headCopyright`, `headDestination`, `headSchemaTagDefs`. | | `clonePerson(person, newIndi)` / `cloneAct(act)` / `person.clone` / `act.clone` | Deep copies for templates or undo stacks. | | `nextFamilyId(persons)` | Next internal family id `F`. | | `createMarriageFamily`, `linkChildToFamily`, `unlinkChildFromFamily`, `removeFamilyReferencesFromDataset` | Create family `F` and spouse acts; optional `{ eventTag }` (default `MARR`, also `ENGA`, banns, contract, `EVEN` + `CreateActInit`, …). See `GEDCOM_7_PAIR_UNION_EVENT_TAGS`. | | `validateReadGed`, `assertReadGedConsistent`, `validatePerson`, `assertPersonConsistent` | Typed `code` / `severity` (`error` \| `warn`). Options: `checkMarrParticipants`, `checkDuplicateIndis`, `checkFamcWithoutSpouses`, `checkFamsWithoutSpouses`, `checkDuplicateFamsEntries`, `checkAncestorCycles`. Assertions: `failOn` (default: `error`). | | `tryAddPersonToReadGed`, `tryRemovePersonFromReadGedByIndi`, `tryLinkChildToFamily`, `tryUnlinkChildFromFamily`, `tryCreateMarriageFamily`, `commandBlockingIssues` | Command layer with `CommandResult` / `validate*Command` prechecks. | ### Utilities #### `createSosaMap(root, partnersMap)` Computes a Sosa numbering (Ahnentafel) starting at `root` (Sosa 1) and recursively walking ancestors via `partnersMap`. ```ts import { createSosaMap, Person } from "gedcom-ts"; const sosaMap = createSosaMap(root, readGed.partnersMap); // e.g. sosaMap.get(root.INDI) === 1 ``` #### `remainingTypesAct(acts, actToUpdate?)` 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. ```ts import { remainingTypesAct, Acts } from "gedcom-ts"; const available = remainingTypesAct(new Acts()); ``` #### `getCityCoordinates(cityName, callback)` (deprecated) Legacy callback API. Use [Geocoding places](#geocoding-places) instead. #### `resolveDatasetVersion(headerLines)` / `GedcomDatasetVersion` Inspects the lines of a `0 HEAD` block and returns `"7.0"`, `"5.5"` or `"unknown"`. Useful to branch UI behaviour for legacy datasets. ```ts import { resolveDatasetVersion } from "gedcom-ts"; const version = resolveDatasetVersion(headLines); // "7.0" | "5.5" | "unknown" ``` #### `guessMediaFormFromUri(uri)` Best-effort media type detection from a path/URI or local `File` (`file.type` when set). Covers images (incl. **tiff**, **heic**, svg), **audio** (mp3, **webm**, ogg, wav, m4a, flac, …), **video** (mp4, mov, mkv, …), subtitles (vtt, srt), and documents (pdf, djvu, docx, odt, xlsx, csv, html). Used to fill `OBJE`.`FILE`.`FORM`. Pair with `guessMediFromForm` for `MEDI` (`AUDIO`, `PHOTO`, `VIDEO`, `ELECTRONIC`, …). `refineGenericObjeForm` upgrades legacy `application/octet-stream` exports using the file extension. ```ts import { guessMediaFormFromUri, guessMediaFormFromFile } from "gedcom-ts"; guessMediaFormFromUri("media/recording.webm"); // "audio/webm" guessMediaFormFromFile(new File(["x"], "take.webm", { type: "audio/webm" })); // "audio/webm" ``` #### `extractPersonNameVariants(personLines)` / `extractIndiAttributes(personLines)` 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`. ```ts import { extractPersonNameVariants, extractIndiAttributes } from "gedcom-ts"; const variants = extractPersonNameVariants(rawIndiLines); const attributes = extractIndiAttributes(rawIndiLines); ``` #### `selectPrimaryNameVariant(variants)` Picks the most relevant `1 NAME` block: priority to `2 TYPE BIRTH`, then to a name with a `/surname/` payload, otherwise the first variant. ```ts import { selectPrimaryNameVariant } from "gedcom-ts"; const primary = selectPrimaryNameVariant(person.nameVariants); ``` #### `GEDCOM_LIBRARY_VERSION` CalVer string written in exported `HEAD`.`SOUR`.`VERS` (same value as the npm package version, e.g. `2026.6.4`). ```ts import { GEDCOM_LIBRARY_VERSION } from "gedcom-ts"; console.log(`gedcom-ts ${GEDCOM_LIBRARY_VERSION}`); ``` ## Error handling ```ts import { importGedFile, IMPORT_ERR_ZIP_GED_UNREADABLE } from "gedcom-ts"; try { const readGed = await importGedFile(file); console.log(readGed.persons.length); } catch (error) { if ( error instanceof Error && error.message === IMPORT_ERR_ZIP_GED_UNREADABLE ) { console.error("Encrypted ZIP: please extract the .ged manually."); } else { console.error("GED import failed:", error); } } ```