diff --git a/packages/serialization/src/mmeaf-parse.ts b/packages/serialization/src/mmeaf-parse.ts index faeb53c..ea9fd21 100644 --- a/packages/serialization/src/mmeaf-parse.ts +++ b/packages/serialization/src/mmeaf-parse.ts @@ -12,7 +12,7 @@ import { XMLParser } from 'fast-xml-parser' import { newId } from '@mumo/core' -import type { PatternSchemaJSON, PatternJSON, AnnotationJSON, AnchorJSON, TokenRecord, TierDefJSON, SymbolDef, ParticipantJSON, Suggestion, SuggestedChange, TextletCode, SlotInstance, MetricValue, NoteEntry } from '@mumo/core' +import type { PatternSchemaJSON, PatternJSON, AnnotationJSON, AnchorJSON, TokenRecord, TierDefJSON, SymbolDef, ParticipantJSON, Suggestion, SuggestedChange, TextletCode, SlotInstance, MetricValue, MetricType, NoteEntry } from '@mumo/core' import { parseEAF } from './eaf-parse.js' import type { ParseResult } from './eaf-parse.js' import type { PMNodeJSON } from './types.js' @@ -85,6 +85,18 @@ function parseFeatureElems(els: Rec[]): Record { return out } +// Metric values are stored as XML attributes, so fast-xml-parser returns them +// as strings. Decode them using their metric schema (as required by the MMEAF +// schema) so boolean checkboxes receive actual booleans after a file reload. +function parseMetricValue(el: Rec, metricTypes: ReadonlyMap): MetricValue { + const schemaId = ga(el, 'schema_id') ?? '' + const raw = ga(el, 'value') ?? '' + return { + schemaId, + value: metricTypes.get(schemaId) === 'boolean' ? raw === 'true' : raw, + } +} + // Helpers function applyAnnotationMarkToContent(content: PMNodeJSON[], markId: string, start: number, end: number): PMNodeJSON[] { @@ -667,15 +679,20 @@ export function parseMMEAF(xml: string): MMEAFParseResult { }) } + const metricTypes = new Map() + for (const schema of patternSchemas) { + for (const slot of schema.slots) { + for (const metric of slot.metrics) metricTypes.set(metric.id, metric.type) + } + } + // Patterns const patternsEl = mmDataEl['mm:patterns'] as Rec | undefined const patterns: PatternJSON[] = [] for (const el of (((patternsEl?.['mm:pattern'] ?? [])) as Rec[])) { const slots = (((el['mm:slot_instance'] ?? [])) as Rec[]).map(siEl => { - const metrics = (((siEl['mm:metric_value'] ?? [])) as Rec[]).map(mvEl => ({ - schemaId: ga(mvEl, 'schema_id') ?? '', - value: ga(mvEl, 'value') ?? '', - })) + const metrics = (((siEl['mm:metric_value'] ?? [])) as Rec[]) + .map(mvEl => parseMetricValue(mvEl, metricTypes)) return { id: ga(siEl, 'id') ?? newId(), schemaSlotId: ga(siEl, 'schema_slot_id') ?? '', @@ -985,10 +1002,8 @@ export function parseMMEAF(xml: string): MMEAFParseResult { const slotId = ga(siEl, 'id') ?? newId() const schemaSlotId = ga(siEl, 'schema_slot_id') ?? '' const annotationId = ga(siEl, 'annotation_id') ?? '' - const metrics: MetricValue[] = ((siEl['mm:metric_value'] ?? []) as Rec[]).map(mv => ({ - schemaId: ga(mv, 'schema_id') ?? '', - value: ga(mv, 'value') ?? '', - })) + const metrics: MetricValue[] = ((siEl['mm:metric_value'] ?? []) as Rec[]) + .map(mv => parseMetricValue(mv, metricTypes)) const slot: SlotInstance = { id: slotId, schemaSlotId, annotationId, metrics } let pendingAnnotation: AnnotationJSON | undefined diff --git a/packages/serialization/tests/mmeaf-roundtrip.test.ts b/packages/serialization/tests/mmeaf-roundtrip.test.ts index 714ac93..839ad4a 100644 --- a/packages/serialization/tests/mmeaf-roundtrip.test.ts +++ b/packages/serialization/tests/mmeaf-roundtrip.test.ts @@ -383,6 +383,40 @@ describe('patterns round-trip', () => { expect(mv.value).toBe('NOUN') }) + it('decodes boolean metric values using their metric schema', () => { + const store = new AnnotationStore() + const schemaId = newId() + const slotId = newId() + const trueMetricId = newId() + const falseMetricId = newId() + const textMetricId = newId() + + store.addPatternSchema({ + name: 'Test', slots: [{ + id: slotId, name: 's', anchorKind: 'textlet', + metrics: [ + { id: trueMetricId, name: 'checked', type: 'boolean' }, + { id: falseMetricId, name: 'unchecked', type: 'boolean' }, + { id: textMetricId, name: 'literal', type: 'text' }, + ], + }], + }, schemaId) + store.addPattern(schemaId, [{ + id: newId(), schemaSlotId: slotId, annotationId: newId(), metrics: [ + { schemaId: trueMetricId, value: true }, + { schemaId: falseMetricId, value: false }, + { schemaId: textMetricId, value: 'true' }, + ], + }]) + + const metrics = parseMMEAF(emitMMEAF(emptyDoc(), store)).patterns[0]!.slots[0]!.metrics + expect(metrics).toEqual([ + { schemaId: trueMetricId, value: true }, + { schemaId: falseMetricId, value: false }, + { schemaId: textMetricId, value: 'true' }, + ]) + }) + it('emits no FRAMES block when store has none', () => { const xml = emitMMEAF(emptyDoc(), new AnnotationStore()) expect(xml).not.toContain('')