diff --git a/bitext/src/lib/components/examples/ExamplePreviewEmbed.svelte b/bitext/src/lib/components/examples/ExamplePreviewEmbed.svelte index e4c6a7e..f148737 100644 --- a/bitext/src/lib/components/examples/ExamplePreviewEmbed.svelte +++ b/bitext/src/lib/components/examples/ExamplePreviewEmbed.svelte @@ -5,7 +5,6 @@ import { findExample, type ExampleId } from '$lib/state/examples.js'; import { layoutExportStore } from '$lib/state/layoutExport.svelte.js'; import { projectStore } from '$lib/state/project.svelte.js'; - import { settingsStore } from '$lib/state/settings.svelte.js'; let { exampleId, @@ -20,10 +19,9 @@ onMount(() => { void (async () => { - const state = buildAppStateFromExample(findExample(exampleId), { previewHideChrome: true }); - settingsStore.load(state.settings); - projectStore.loadSnapshotV2(state.project); - projectStore.retokenizeFromSettings(); + projectStore.loadState( + buildAppStateFromExample(findExample(exampleId), { previewHideChrome: true }) + ); layoutExportStore.requestRemeasureAfterLayout(); await document.fonts.ready; await tick(); diff --git a/bitext/src/lib/serialization/compact-v4.ts b/bitext/src/lib/serialization/compact-v4.ts index fe2daa8..e031c9e 100644 --- a/bitext/src/lib/serialization/compact-v4.ts +++ b/bitext/src/lib/serialization/compact-v4.ts @@ -75,11 +75,11 @@ function settingsToCompact(rounded: VisualSettingsV2): CompactSettings4 | undefi if (rounded.background !== def.background) { o.bg = rounded.background === 'dark' ? 1 : 0; } - const keysBeforeFinalize = Object.keys(o).length; - if (keysBeforeFinalize > 0) { + // Decoding treats a present `s` without `mg` as "no merge char", so any emitted `s` carries it. + if (Object.keys(o).length > 0 || rounded.tokenMergeChar !== def.tokenMergeChar) { o.mg = rounded.tokenMergeChar; } - return keysBeforeFinalize > 0 ? sortKeys(o) : undefined; + return Object.keys(o).length > 0 ? sortKeys(o) : undefined; } function compactToVisualSettings(s: CompactSettings4 | undefined): VisualSettingsV2 { diff --git a/bitext/src/lib/serialization/serialization-roundtrip.test.ts b/bitext/src/lib/serialization/serialization-roundtrip.test.ts index b6e4543..b57bf2a 100644 --- a/bitext/src/lib/serialization/serialization-roundtrip.test.ts +++ b/bitext/src/lib/serialization/serialization-roundtrip.test.ts @@ -284,6 +284,14 @@ describe('compact v4 encode/decode (current share format)', () => { expect(decoded.settings.tokenSplitChars).toBe('.'); }); + it('round-trip: a changed merge char survives when it is the only non-default setting', () => { + const base = migrate({}); + for (const tokenMergeChar of ['_', '']) { + const s: AppStateV2 = { ...base, settings: { ...base.settings, tokenMergeChar } }; + expect(decodeState(encodeState(s)).settings.tokenMergeChar).toBe(tokenMergeChar); + } + }); + it('round-trip: token link color mode (background)', () => { const base = migrate({}); const s: AppStateV2 = { diff --git a/bitext/src/lib/state/project-load-state.test.ts b/bitext/src/lib/state/project-load-state.test.ts new file mode 100644 index 0000000..c7c067e --- /dev/null +++ b/bitext/src/lib/state/project-load-state.test.ts @@ -0,0 +1,65 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { projectStore } from './project.svelte.js'; +import { settingsStore } from './settings.svelte.js'; +import { decodeState } from '$lib/serialization/decode.js'; +import { encodeState } from '$lib/serialization/encode.js'; +import { + SCHEMA_VERSION, + defaultVisualSettingsV2, + type AppStateV2, + type LineV2 +} from '$lib/serialization/schema.js'; + +function line(id: string, rawText: string): LineV2 { + return { + id, + rawText, + font: { family: 'Inter', source: 'google' }, + textSizePx: 36, + gapWordPx: 14 + }; +} + +/** Middle line only splits into three tokens with the custom `-` separator. */ +function stateWithCustomSeparator(): AppStateV2 { + return { + v: SCHEMA_VERSION, + settings: { ...defaultVisualSettingsV2(), tokenSplitChars: '|-' }, + project: { + lines: [line('s', 'a b c'), line('t', 'x-y-z'), line('u', 'd e f')], + connections: [ + { id: 'c1', upperTokenId: 's-0', lowerTokenId: 't-0' }, + { id: 'c2', upperTokenId: 's-1', lowerTokenId: 't-1' }, + { id: 'c3', upperTokenId: 's-2', lowerTokenId: 't-2' }, + { id: 'c4', upperTokenId: 't-0', lowerTokenId: 'u-2' }, + { id: 'c5', upperTokenId: 't-1', lowerTokenId: 'u-1' }, + { id: 'c6', upperTokenId: 't-2', lowerTokenId: 'u-0' } + ], + pairControls: [], + linePairGaps: [] + } + }; +} + +describe('project store: loadState', () => { + beforeEach(() => { + settingsStore.reset(); + projectStore.loadSnapshotV2({ + lines: [line('s', 'a'), line('t', 'b')], + connections: [], + pairControls: [], + linePairGaps: [] + }); + }); + + it('keeps links on tokens produced by the loaded tokenization settings', () => { + const shared = decodeState(encodeState(stateWithCustomSeparator())); + expect(shared.project.connections).toHaveLength(6); + + projectStore.loadState(shared); + + expect(settingsStore.settings.tokenSplitChars).toBe('|-'); + expect(projectStore.tokensOnLine('t').map((t) => t.text)).toEqual(['x', 'y', 'z']); + expect(projectStore.connections).toHaveLength(6); + }); +}); diff --git a/bitext/src/lib/state/project.svelte.ts b/bitext/src/lib/state/project.svelte.ts index b53c17a..74ed139 100644 --- a/bitext/src/lib/state/project.svelte.ts +++ b/bitext/src/lib/state/project.svelte.ts @@ -25,6 +25,7 @@ import { MAX_LINES, NEW_LINE_HINT_TEXT, normalizeProjectSnapshotV2, + type AppStateV2, type LinePairGapV2, type LineV2, type PairControlV2, @@ -363,11 +364,14 @@ class ProjectStore { this.pruneInvalidConnections(); } - loadExample(kind: ExampleId = 'simple') { - const state = buildAppStateFromExample(findExample(kind)); + /** Settings load first: connections are pruned against tokens built from them. */ + loadState(state: AppStateV2) { settingsStore.load(state.settings); this.loadSnapshotV2(state.project); - this.retokenizeFromSettings(); + } + + loadExample(kind: ExampleId = 'simple') { + this.loadState(buildAppStateFromExample(findExample(kind))); layoutExportStore.requestRemeasureAfterLayout(); } } diff --git a/bitext/src/routes/+page.svelte b/bitext/src/routes/+page.svelte index 35d6d13..5b0ed26 100644 --- a/bitext/src/routes/+page.svelte +++ b/bitext/src/routes/+page.svelte @@ -70,9 +70,7 @@ $effect(() => { if (hydrated) return; if (data.initialState) { - projectStore.loadSnapshotV2(data.initialState.project); - settingsStore.load(data.initialState.settings); - projectStore.retokenizeFromSettings(); + projectStore.loadState(data.initialState); } hydrated = true; });