-
Notifications
You must be signed in to change notification settings - Fork 36
Expand file tree
/
Copy pathgedcom.ts
More file actions
332 lines (292 loc) · 9.18 KB
/
Copy pathgedcom.ts
File metadata and controls
332 lines (292 loc) · 9.18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
/// <reference path="parse-gedcom.d.ts" />
import { GedcomEntry, parse as parseGedcom } from 'parse-gedcom';
import {
Date,
DateOrRange,
JsonEvent,
JsonFam,
JsonGedcomData,
JsonIndi,
JsonImage,
} from './data';
/** Returns the first entry with the given tag or undefined if not found. */
function findTag(tree: GedcomEntry[], tag: string): GedcomEntry | undefined {
return tree.find((entry) => entry.tag === tag);
}
/** Returns all entries with the given tag. */
function findTags(tree: GedcomEntry[], tag: string): GedcomEntry[] {
return tree.filter((entry) => entry.tag === tag);
}
/**
* Returns the identifier extracted from a pointer string.
* E.g. '@I123@' -> 'I123'
*/
function pointerToId(pointer: string): string {
return pointer.substring(1, pointer.length - 1);
}
/** Extracts the first and last name from a GEDCOM name field. */
function extractName(name: string): { firstName?: string; lastName?: string } {
const matches = name.match(/\/([^\/]+)\//);
if (matches) {
const lastName = matches[1].trim();
// Replace the last name part (including slashes) and trim the result
let firstName = name.replace(matches[0], '').trim();
// Replace any occurrence of double spaces with a single space
firstName = firstName.replace(/\s\s+/g, ' ');
return {
firstName: firstName || undefined,
lastName: lastName
};
} else {
// If no last name is found, just return the trimmed and cleaned firstName
return { firstName: name.trim().replace(/\s\s+/g, ' ') };
}
}
/** Maps month abbreviations used in GEDCOM to month numbers. */
const MONTHS: Map<string, number> = new Map([
['jan', 1],
['feb', 2],
['mar', 3],
['apr', 4],
['may', 5],
['jun', 6],
['jul', 7],
['aug', 8],
['sep', 9],
['oct', 10],
['nov', 11],
['dec', 12],
]);
/** Parses the GEDCOM date into the Date structure. */
function parseDate(parts: string[]): Date | undefined {
if (!parts || !parts.length) {
return undefined;
}
const result: Date = {};
// Remove parentheses if they surround the text.
if (parts[0].startsWith('(') && parts[parts.length - 1].endsWith(')')) {
parts[0] = parts[0].substring(1);
const lastPart = parts[parts.length - 1];
parts[parts.length - 1] = lastPart.substring(0, lastPart.length - 1);
}
const fullText = parts.join(' ');
const firstPart = parts[0].toLowerCase();
if (firstPart === 'cal' || firstPart === 'abt' || firstPart === 'est') {
result.qualifier = firstPart;
parts = parts.slice(1);
}
if (parts.length && parts[parts.length - 1].match(/^\d{1,4}$/)) {
result.year = Number(parts[parts.length - 1]);
parts = parts.slice(0, parts.length - 1);
}
if (parts.length) {
const lastPart = parts[parts.length - 1].toLowerCase();
if (MONTHS.has(lastPart)) {
result.month = MONTHS.get(lastPart);
parts = parts.slice(0, parts.length - 1);
}
}
if (parts.length && parts[0].match(/^\d\d?$/)) {
result.day = Number(parts[0]);
parts = parts.slice(0, parts.length - 1);
}
if (parts.length) {
// A part didn't get parsed. Return the whole text verbatim.
return { text: fullText };
}
return result;
}
/** Parses a GEDCOM date or date range. */
export function getDate(gedcomDate: string): DateOrRange | undefined {
const parts = gedcomDate.replace(/@.*@/, '').trim().split(' ');
const firstPart = parts[0].toLowerCase();
if (firstPart.startsWith('bet')) {
const i = parts.findIndex((x) => x.toLowerCase() === 'and');
const from = parseDate(parts.slice(1, i));
const to = parseDate(parts.slice(i + 1));
return { dateRange: { from, to } };
}
if (firstPart.startsWith('bef') || firstPart.startsWith('aft')) {
const date = parseDate(parts.slice(1));
if (firstPart.startsWith('bef')) {
return { dateRange: { to: date } };
}
return { dateRange: { from: date } };
}
const date = parseDate(parts);
if (date) {
return { date };
}
return undefined;
}
/**
* tries to treat an input tag as NOTE and parsse all lines of notes
*/
function createNotes(notesTag: GedcomEntry | undefined): string[] | undefined {
if (!notesTag || notesTag.tag !== 'NOTE') return undefined;
return findTags(notesTag.tree, 'CONT')
.filter((x) => x.data)
.reduce((a, i) => a.concat(i.data), [notesTag.data]);
}
/**
* Creates a JsonEvent object from a GEDCOM entry.
* Used for BIRT, DEAT and MARR tags.
*/
function createEvent(entry: GedcomEntry | undefined): JsonEvent | undefined {
if (!entry) {
return undefined;
}
const typeTag = findTag(entry.tree, 'TYPE');
const dateTag = findTag(entry.tree, 'DATE');
const placeTag = findTag(entry.tree, 'PLAC');
const date = dateTag && dateTag.data && getDate(dateTag.data);
const place = placeTag && placeTag.data;
if (date || place) {
const result: JsonEvent = date || {};
if (place) {
result.place = place;
}
result.confirmed = true;
result.type = typeTag ? typeTag!.data : undefined;
result.notes = createNotes(findTag(entry.tree, 'NOTE'));
return result;
}
if (entry.data && entry.data.toLowerCase() === 'y') {
return { confirmed: true };
}
return undefined;
}
/** Creates a JsonIndi object from an INDI entry in GEDCOM. */
function createIndi(
entry: GedcomEntry,
objects: Map<string, GedcomEntry>
): JsonIndi {
const id = pointerToId(entry.pointer);
const fams = findTags(entry.tree, 'FAMS').map((entry) =>
pointerToId(entry.data)
);
const indi: JsonIndi = { id, fams };
// Name.
const nameTags = findTags(entry.tree, 'NAME');
const isMaiden = (nameTag: GedcomEntry) => {
const type = findTag(nameTag.tree, 'TYPE');
return type !== undefined && type.data === 'maiden';
};
const main = nameTags.find((x) => !isMaiden(x));
const maiden = nameTags.find(isMaiden);
if (main) {
const { firstName, lastName } = extractName(main.data);
if (firstName) {
indi.firstName = firstName;
}
if (lastName) {
indi.lastName = lastName;
}
}
if (maiden) {
const { firstName, lastName } = extractName(maiden.data);
if (lastName) {
indi.maidenName = lastName;
}
if (firstName && !indi.firstName) {
indi.firstName = firstName;
}
}
// Number of children.
const nchiTag = findTag(entry.tree, 'NCHI');
if (nchiTag) {
indi.numberOfChildren = +nchiTag.data;
}
// Number of marriages.
const nmrTag = findTag(entry.tree, 'NMR');
if (nmrTag) {
indi.numberOfMarriages = +nmrTag.data;
}
// Sex.
const sexTag = findTag(entry.tree, 'SEX');
if (sexTag) {
indi.sex = sexTag.data;
}
// Family with parents.
const famcTag = findTag(entry.tree, 'FAMC');
if (famcTag) {
indi.famc = pointerToId(famcTag.data);
}
// Image URL.
const objeTags = findTags(entry.tree, 'OBJE');
if (objeTags.length > 0) {
// Dereference OBJEct if needed.
const getFileTag = (tag: GedcomEntry) => {
const realObjeTag = tag.data ? objects.get(pointerToId(tag.data)) : tag;
if (!realObjeTag) return undefined;
const file = findTag(realObjeTag.tree, 'FILE');
const title = findTag(realObjeTag.tree, 'TITL');
if (!file) return undefined;
return {
url: file.data,
title: title && title.data,
} as JsonImage;
};
indi.images = objeTags
.map(getFileTag)
.filter((x): x is JsonImage => x !== undefined);
}
// Birth date and place.
const birth = createEvent(findTag(entry.tree, 'BIRT'));
if (birth) {
indi.birth = birth;
}
// Death date and place.
const death = createEvent(findTag(entry.tree, 'DEAT'));
if (death) {
indi.death = death;
}
// Notes.
indi.notes = createNotes(findTag(entry.tree, 'NOTE'));
// Events
indi.events = findTags(entry.tree, 'EVEN')
.map(createEvent)
.filter((x): x is JsonEvent => x !== null);
return indi;
}
/** Creates a JsonFam object from an FAM entry in GEDCOM. */
function createFam(entry: GedcomEntry): JsonFam {
const id = pointerToId(entry.pointer);
const children = findTags(entry.tree, 'CHIL').map((entry) =>
pointerToId(entry.data)
);
const fam: JsonFam = { id, children };
// Husband.
const husbTag = findTag(entry.tree, 'HUSB');
if (husbTag) {
fam.husb = pointerToId(husbTag.data);
}
// Wife.
const wifeTag = findTag(entry.tree, 'WIFE');
if (wifeTag) {
fam.wife = pointerToId(wifeTag.data);
}
// Marriage
const marriage = createEvent(findTag(entry.tree, 'MARR'));
if (marriage) {
fam.marriage = marriage;
}
return fam;
}
/** Creates a map from ID to entry from an array of entries. */
function createMap(entries: GedcomEntry[]): Map<string, GedcomEntry> {
return new Map(entries.map((entry) => [pointerToId(entry.pointer), entry]));
}
/** Parses a GEDCOM file into a JsonGedcomData structure. */
export function gedcomToJson(gedcomContents: string): JsonGedcomData {
return gedcomEntriesToJson(parseGedcom(gedcomContents));
}
/** Converts parsed GEDCOM entries into a JsonGedcomData structure. */
export function gedcomEntriesToJson(gedcom: GedcomEntry[]): JsonGedcomData {
const objects = createMap(findTags(gedcom, 'OBJE'));
const indis = findTags(gedcom, 'INDI').map((entry) =>
createIndi(entry, objects)
);
const fams = findTags(gedcom, 'FAM').map(createFam);
return { indis, fams };
}