diff --git a/backend/src/routes/gedcom.ts b/backend/src/routes/gedcom.ts index 02c691d..563581e 100644 --- a/backend/src/routes/gedcom.ts +++ b/backend/src/routes/gedcom.ts @@ -8,7 +8,7 @@ import { indexGedcomRecords, mediaRefsForPerson, noteText, photoPathForPerson } import type { GedcomRecords } from '../lib/gedcom-photos.js' import type { MembershipRole } from '../types/auth.js' import { nextSiblingOrder } from '../lib/sibling-order.js' -import { sexOrNull } from '../lib/person-sex.js' +import { sexOrNull, type PersonSex } from '../lib/person-sex.js' const treeIdParamSchema = z.object({ id: z.string().min(1), @@ -285,6 +285,15 @@ function pairKey(a: string, b: string | null): string { return b ? [a, b].sort().join('|') : `${a}|` } +// GEDCOM 5.5.1 n'admet que M, F et U (inconnu, autre). « X » n'existe qu'en 7.0 : un sexe « autre » s'écrit U, +// et l'import relit X comme avant. +export function gedcomSexValue(sex: PersonSex | null): 'M' | 'F' | 'U' | null { + if (sex === 'male') return 'M' + if (sex === 'female') return 'F' + if (sex === 'other') return 'U' + return null +} + function sanitizeXref(id: string): string { return id.replace(/[^A-Za-z0-9_]/g, '_') } @@ -456,10 +465,8 @@ export const gedcomRoutes: FastifyPluginAsync = async (app) => { lines.push(`1 NAME ${gedcomNameValue(person.firstName, person.lastName)}`) if (person.nickname) lines.push(`2 NICK ${oneLine(person.nickname)}`) - const sex = sexOrNull(person.sex) - if (sex === 'male') lines.push('1 SEX M') - else if (sex === 'female') lines.push('1 SEX F') - else if (sex === 'other') lines.push('1 SEX X') + const sexValue = gedcomSexValue(sexOrNull(person.sex)) + if (sexValue) lines.push(`1 SEX ${sexValue}`) const personEvents = eventsByPerson.get(person.id) ?? [] const birthDetails = personEvents.find((event) => event.type === 'birth') diff --git a/backend/test/api-routes.test.ts b/backend/test/api-routes.test.ts index 02ea53f..53faa60 100644 --- a/backend/test/api-routes.test.ts +++ b/backend/test/api-routes.test.ts @@ -921,7 +921,7 @@ describe('Sexe dans le GEDCOM (issue #114)', () => { await app.close() }) - it('aller-retour : other sort en SEX X, non précisé sans ligne SEX, et tout se relit à l’identique', async () => { + it('aller-retour : other sort en SEX U (GEDCOM 5.5.1), non précisé sans ligne SEX, le reste se relit à l’identique', async () => { const app = createApp() await app.ready() const token = (app as any).jwt.sign({ kind: 'tree_access', sub: 'tree:tree-1:visitor', treeId: 'tree-1', role: 'visitor', accessVersion: new Date('2026-02-11T10:00:00.000Z').getTime() }) @@ -940,10 +940,12 @@ describe('Sexe dans le GEDCOM (issue #114)', () => { const response = await app.inject({ method: 'GET', url: '/trees/tree-1/export/gedcom', headers: { authorization: `Bearer ${token}` } }) expect(response.statusCode).toBe(200) const text = response.body.replace(/\r\n/g, '\n') - expect(text).toContain('1 NAME Camille /Martin/\n1 SEX X\n') + expect(text).toContain('1 NAME Camille /Martin/\n1 SEX U\n') + expect(text).not.toContain('SEX X') expect(text).toMatch(/1 NAME Claude \/Martin\/\n(?!1 SEX)/) - expect(await importer(app, text)).toEqual({ Jean: 'male', Marie: 'female', Camille: 'other', Claude: null }) + // SEX U se relit comme non précisé : 5.5.1 n'a pas de valeur pour « autre ». SEX X reste lu à l'import (ligne 915). + expect(await importer(app, text)).toEqual({ Jean: 'male', Marie: 'female', Camille: null, Claude: null }) await app.close() }) }) diff --git a/backend/test/gedcom-sex.test.ts b/backend/test/gedcom-sex.test.ts new file mode 100644 index 0000000..3943b2f --- /dev/null +++ b/backend/test/gedcom-sex.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest' + +import { parseSex } from '../src/lib/person-sex' +import { gedcomSexValue, parseGedcom } from '../src/routes/gedcom' + +// Export en GEDCOM 5.5.1 : SEX ne vaut que M, F ou U (#118). « X » n'existe qu'en GEDCOM 7. +describe('export GEDCOM 5.5.1 : valeur SEX', () => { + it('écrit M, F, et U pour un sexe « autre »', () => { + expect(gedcomSexValue('male')).toBe('M') + expect(gedcomSexValue('female')).toBe('F') + expect(gedcomSexValue('other')).toBe('U') + }) + + it("n'écrit jamais X, et ne déclare rien pour un sexe non précisé", () => { + for (const sex of ['male', 'female', 'other', null] as const) { + expect(gedcomSexValue(sex)).not.toBe('X') + } + expect(gedcomSexValue(null)).toBeNull() + }) + + it('l’import relit toujours SEX X comme « autre », et U comme non précisé', () => { + const sexes = parseGedcom(['0 @I1@ INDI', '1 SEX X', '0 @I2@ INDI', '1 SEX U'].join('\n')) + .filter((node) => node.tag === 'INDI') + .map((indi) => parseSex(indi.children.find((child) => child.tag === 'SEX')?.value ?? null)) + expect(sexes).toEqual(['other', null]) + }) +})