Skip to content

Commit 6be5e38

Browse files
Align code with simplified interlinear model
Update components, storage, main, and types to match the model introduced in #63: rename commands, fix return types, update JSDoc, and adjust tests throughout.
1 parent 8db1da2 commit 6be5e38

29 files changed

Lines changed: 464 additions & 240 deletions

AGENTS.md

Lines changed: 72 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,29 @@ This is a **Platform.Bible extension** for interlinear Bible text alignment. Pla
2929

3030
### Extension entry point
3131

32-
`src/main.ts` — called by Platform.Bible on activation. Exports two lifecycle functions:
32+
[src/main.ts](src/main.ts) — called by Platform.Bible on activation. Exports two lifecycle functions:
3333

34-
- `activate(context)` — registers the `interlinearizer.mainWebView` WebView provider, the `interlinearizer.openForWebView` command, and `onDidOpenWebView` / `onDidCloseWebView` subscriptions. All registrations are added to `context.registrations` so the platform disposes them on deactivation.
34+
- `activate(context)`stores the `ExecutionToken`, registers the `interlinearizer.mainWebView` WebView provider, the command handlers below, and the `onDidOpenWebView` / `onDidCloseWebView` subscriptions. All registrations are added to `context.registrations` so the platform disposes them on deactivation.
3535
- `deactivate()` — clears `openWebViewsByProject` and returns `true`.
3636

3737
`openWebViewsByProject` (`Map<string, string>`) tracks one open WebView ID per project to prevent duplicates; reopening an already-open project brings that tab to front via the `existingId` option.
3838

39+
Registered commands:
40+
41+
- `interlinearizer.openForWebView` — opens the Interlinearizer for the WebView's project (or a picker if no ID is given).
42+
- `interlinearizer.createProject` — backend handler that delegates to [projectStorage.createProject](src/services/projectStorage.ts); returns a JSON-serialized `InterlinearProject` or `undefined` on failure.
43+
- `interlinearizer.getProjectsForSource` — returns a JSON-stringified `InterlinearProject[]` filtered by source project.
44+
- `interlinearizer.updateProjectMetadata` — updates name/description/analysisLanguages/targetProjectId for a project.
45+
- `interlinearizer.deleteProject` — deletes a project; no-ops silently when the ID is unknown.
46+
- `interlinearizer.openSelectProjectModal`, `interlinearizer.openNewProjectModal`, `interlinearizer.openProjectInfoModal` — registered server-side as no-op handlers so the platform menu system knows about them; the actual behavior lives in the WebView, which listens for the matching menu-item activation.
47+
3948
### WebView UI
4049

41-
`src/interlinearizer.web-view.tsx` — React component rendered inside Platform.Bible's WebView iframe. `useWebViewScrollGroupScrRef` is a **prop injected by the PAPI host** (not a hook import). Uses PAPI frontend hooks (`useProjectData`, `useProjectSetting`, `useLocalizedStrings`, `useRecentScriptureRefs`) to fetch live data. Renders verse segments as token chips with Tailwind utility classes (all prefixed `tw:`).
50+
[src/interlinearizer.web-view.tsx](src/interlinearizer.web-view.tsx) is the entry point that the PAPI host renders inside its WebView iframe. It just delegates to [InterlinearizerLoader](src/components/InterlinearizerLoader.tsx) when a `projectId` is present. `useWebViewScrollGroupScrRef` and `useWebViewState` are **props injected by the PAPI host** (not hook imports).
51+
52+
[InterlinearizerLoader](src/components/InterlinearizerLoader.tsx) is the real top of the React tree: it owns modal state, persists the active interlinear project via `useWebViewState`, fetches and tokenizes book data, renders the `TabToolbar` + `ScriptureNavControls` + `ContinuousScrollToggle`, and routes top-menu commands (`openSelectProjectModal` / `openNewProjectModal` / `openProjectInfoModal`) to the appropriate modal. The "View Project Info" item is filtered out of the menu when no project is active.
53+
54+
[Interlinearizer](src/components/Interlinearizer.tsx) renders the actual interlinear view: an optional `ContinuousView` strip above a list of `SegmentView`s for the current chapter.
4255

4356
The WebView is injected into the main bundle via Webpack's `?inline` query:
4457

@@ -47,37 +60,74 @@ import interlinearizerReact from './interlinearizer.web-view?inline';
4760
import interlinearizerStyles from './interlinearizer.web-view.scss?inline';
4861
```
4962

50-
`src/webpack-env.d.ts` declares the `*?inline`, `*?raw`, and `*.scss` module types that make these imports type-safe.
63+
[src/webpack-env.d.ts](src/webpack-env.d.ts) declares the `*?inline`, `*?raw`, and `*.scss` module types that make these imports type-safe.
5164

5265
Two separate Webpack configs handle this: `webpack.config.web-view.ts` builds the React component into `temp-build/`, then `webpack.config.main.ts` copies it into `dist/` alongside contributions, public assets, and type declarations.
5366

5467
The WebView root component is assigned to `globalThis.webViewComponent` (not exported) — this is the PAPI WebView contract. Tests must `require()` the module and read `globalThis.webViewComponent` to get the component.
5568

69+
### Project modals
70+
71+
[src/components/ProjectModals.tsx](src/components/ProjectModals.tsx) is the single mount point for all project-related dialogs; it switches between `'select' | 'create' | 'metadata' | 'none'` based on a `modal` prop owned by `InterlinearizerLoader`:
72+
73+
- [SelectInterlinearProjectModal](src/components/SelectInterlinearProjectModal.tsx) — lists existing projects for the source via `interlinearizer.getProjectsForSource`, with an info icon that opens the metadata modal.
74+
- [CreateProjectModal](src/components/CreateProjectModal.tsx) — collects name, description, and analysis-language tags, then calls `interlinearizer.createProject`.
75+
- [ProjectMetadataModal](src/components/ProjectMetadataModal.tsx) — edits / deletes an existing project via `interlinearizer.updateProjectMetadata` and `interlinearizer.deleteProject`.
76+
77+
The active project is persisted in WebView state under the `activeProject` key so it survives tab restores. The `isInterlinearProjectSummary` type guard in `SelectInterlinearProjectModal.tsx` validates JSON parsed from backend commands without `as` casts.
78+
79+
### Project storage
80+
81+
[src/services/projectStorage.ts](src/services/projectStorage.ts) owns all `papi.storage` reads and writes for interlinearizer projects:
82+
83+
- Projects are persisted under the `project:{uuid}` key; the ordered list of all UUIDs lives at `projectIds`.
84+
- Two serialization queues prevent interleaved read-modify-write races: `indexQueue` guards the `projectIds` index and a per-project `projectQueues` map guards each project's record. `createProject` rolls the project write back when the index update fails.
85+
- ENOENT (`isNotFound`) is treated as "key has never been written" rather than an error — used for both project reads and the initial-empty-index case.
86+
- Tests must call `resetQueuesForTesting()` between tests because module state is not cleared by `resetMocks`.
87+
5688
### Styling
5789

5890
All UI uses Tailwind CSS (via `src/tailwind.css`). Every Tailwind class is prefixed `tw:` to avoid collisions with Platform.Bible's own styles (configured in `tailwind.config.ts`). For modifier variants the prefix comes first: `tw:hover:px-3`, not `hover:tw-px-3`.
5991

92+
### Hooks
93+
94+
[src/hooks/useInterlinearizerBookData.ts](src/hooks/useInterlinearizerBookData.ts) — orchestrates the per-project book pipeline. Reads USJ via `useProjectData('platformScripture.USJ_Book', projectId)` and the writing system via `useProjectSetting('platform.languageTag', ...)`, runs them through `extractBookFromUsj` and `tokenizeBook`, and returns `{ book, chapterSegments, isLoading, bookError, tokenizeError }`. The hook only depends on `scrRef.book` (not chapter/verse) for loading; chapter scoping happens during filtering.
95+
96+
[src/hooks/useOptimisticBooleanSetting.ts](src/hooks/useOptimisticBooleanSetting.ts) — wraps `useProjectSetting` with optimistic UI: a toggle update is shown immediately, the platform's confirmation is ignored for `TIMEOUT_MS` (15s) so the toggle does not visibly bounce, and if the platform never confirms the optimistic value persists rather than reverting.
97+
6098
### Parser pipeline
6199

62100
Data flows from Platform.Bible's USJ (Unified Scripture JSON) format through two stages:
63101

64-
1. `src/parsers/papi/usjBookExtractor.ts` — converts USJ to the internal `Book` type
65-
2. `src/parsers/papi/bookTokenizer.ts` — segments and tokenizes the book into `Segment`/`Token` structures with character offsets
102+
1. [src/parsers/papi/usjBookExtractor.ts](src/parsers/papi/usjBookExtractor.ts) — walks USJ nodes (book / chapter / verse / para / note) into a `RawBook` (`bookCode`, `writingSystem`, `contentHash`, `verses`). Heading-class `para` markers are dropped so their text never bleeds into the verse baseline; `note` content is also skipped. The `contentHash` is an FNV-1a 32-bit hash of a stably-stringified `usj.content`, used as `Book.textVersion` to detect baseline drift.
103+
2. [src/parsers/papi/bookTokenizer.ts](src/parsers/papi/bookTokenizer.ts) — segments and tokenizes the book into `Segment`/`Token` structures. The tokenizer regex uses Unicode property classes (`\p{L}\p{N}\p{M}\p{Join_Control}`), absorbs apostrophes/right-single-quotes at word edges (for languages where they mark phonemic glottal stops), and treats `'`, `-`, Unicode dashes, and `` as word-internal joiners only when surrounded by word characters. Whitespace is not tokenized; the invariant `Segment.baselineText.slice(charStart, charEnd) === Token.surfaceText` is preserved.
104+
105+
[src/parsers/pt9/interlinearXmlParser.ts](src/parsers/pt9/interlinearXmlParser.ts) — separately parses Paratext 9 interlinear XML via `fast-xml-parser`. Strict mode for clusters (required `Range` with non-negative integer `Index`/`Length`; lexemes require `Id`), lenient for punctuation (entries with missing/invalid `Range` are silently filtered). The XML schema is documented in [pt9-xml.md](src/parsers/pt9/pt9-xml.md). The exported `InterlinearXmlParser` class holds one configured `XMLParser` — reuse a single instance across `parse()` calls.
106+
107+
### Data model ([src/types/interlinearizer.d.ts](src/types/interlinearizer.d.ts))
108+
109+
The file declares two ambient modules:
66110

67-
`src/parsers/pt9/interlinearXmlParser.ts` — separately parses Paratext 9 interlinear XML into the alignment model. The XML schema is documented in `src/parsers/pt9/pt9-xml.md`.
111+
- `papi-shared-types` — augments `ProjectSettingTypes` (`interlinearizer.continuousScroll: boolean`) and `CommandHandlers` (the seven interlinearizer commands described above) so Platform.Bible's typed APIs know about them.
112+
- `interlinearizer` — the project's domain types.
68113

69-
### Data model (`src/types/interlinearizer.d.ts`)
114+
The core domain types are:
70115

71-
The core types are:
116+
- `InterlinearProject` — persisted envelope: id, createdAt, optional name/description, `sourceProjectId`, optional `targetProjectId`, `analysisLanguages`, `analysis: TextAnalysis`, and `links?: AlignmentLink[]`. Only this is serialized to storage; the `Book` hierarchy is rebuilt from USJ on each load.
117+
- `ActiveProject` — runtime pairing of `project: InterlinearProject` with reconstructed `source: Book[]` and optional `target: Book[]`.
118+
- `Book → Segment → Token` — the text hierarchy.
119+
- `TextAnalysis` — flat analysis layer keyed by id (does **not** mirror the text hierarchy). Holds `segmentAnalyses`, `tokenAnalyses`, `phraseAnalyses`, plus a `*Links` array for each that attaches an analysis record to one or more text-layer targets.
120+
- `TokenAnalysis / MorphemeAnalysis` — parse and 1:1 glosses; multiple analyses per token are allowed, distinguished by `status`.
121+
- `AlignmentLink` — directional links between source and target endpoints.
122+
- `AlignmentEndpoint` — has either token-level or morpheme-level specificity via the optional `morphemeLink` field (when present, both `tokenAnalysisId` and `morphemeId` are required).
123+
- `EntryRef` / `SenseRef` / `AllomorphRef` / `GrammarRef` — references to the Lexicon extension. The file documents current gaps in `IEntryService` that the Lexicon extension is expected to close.
72124

73-
- `InterlinearAlignment` — top-level bilingual container (source + target `InterlinearText`)
74-
- `Book → Segment → Token` — the text hierarchy
75-
- `TextAnalysis` — flat analysis layer keyed by id (does **not** mirror text hierarchy)
76-
- `TokenAnalysis / Morpheme` — parse and 1:1 glosses; multiple analyses per token are allowed, distinguished by `status`
77-
- `AlignmentLink` — links between source and target tokens/morphemes
78-
- `AlignmentEndpoint` — has either token-level or morpheme-level specificity, never both
125+
Key invariants:
79126

80-
Key invariants: `Segment.baselineText.slice(charStart, charEnd) === Token.surfaceText`; at most one `TokenAnalysis` per token may have `status: 'approved'`. Multi-string content is tagged by BCP47 writing-system codes. `tokenSnapshot` fields detect drift when baseline text changes.
127+
- `Segment.baselineText.slice(charStart, charEnd) === Token.surfaceText`.
128+
- At most one linked `TokenAnalysisLink` per token may have `status: 'approved'`; same for `SegmentAnalysisLink` per segment and for `PhraseAnalysisLink` per token covered.
129+
- `MultiString` values are keyed by BCP 47 tags.
130+
- `TokenSnapshot.surfaceText` is the drift-detection mechanism: when it no longer matches the current `Token.surfaceText`, consumers flip the link's `status` to `'stale'`.
81131

82132
### TypeScript path aliases
83133

@@ -103,12 +153,13 @@ Key semantic properties of the mock setup:
103153
Mock files:
104154

105155
- **`__mocks__/fileMock.ts`** — Stub static asset imports.
106-
- **`__mocks__/papi-backend.ts`** — Mocks with jest fns. Re-exports internal jest fns on the default export as `__mock*` properties (e.g., `papi.__mockRegisterCommand`) so tests can assert on them without re-importing. See file for full list.
156+
- **`__mocks__/lucide-react.tsx`** — Stubs the icon components used in modals (`Info`, `Trash2`).
157+
- **`__mocks__/papi-backend.ts`** — Mocks with jest fns including `commands`, `dialogs`, `notifications`, `storage`, `webViewProviders`, and `webViews`. Re-exports internal jest fns on the default export as `__mock*` properties (e.g., `papi.__mockRegisterCommand`, `papi.__mockReadUserData`) so tests can assert on them without re-importing. See file for the full list.
107158
- **`__mocks__/papi-core.ts`** — Empty module; exists only for module resolution since `@papi/core` is types-only at runtime.
108-
- **`__mocks__/papi-frontend.ts`** — Stubs `logger` (debug/error/info/warn as jest fns).
109-
- **`__mocks__/papi-frontend-react.ts`** — Stubs PAPI React hooks.
110-
- **`__mocks__/platform-bible-react.tsx`** — Stubs components with appropriate `data-testid` attributes. See file for test IDs.
111-
- **`__mocks__/platform-bible-utils.ts`** — Stubs util functions.
159+
- **`__mocks__/papi-frontend.ts`** — Stubs `logger` (debug/error/info/warn as jest fns) and `papi.commands.sendCommand` / `papi.notifications.send`.
160+
- **`__mocks__/papi-frontend-react.ts`** — Stubs PAPI React hooks (`useData`, `useProjectData`, `useProjectSetting`, `useLocalizedStrings`, `useRecentScriptureRefs`).
161+
- **`__mocks__/platform-bible-react.tsx`** — Stubs components (`TabToolbar`, `BookChapterControl`, `ScrollGroupSelector`, `Button`, `Switch`, `Label`) with appropriate `data-testid` attributes. See file for test IDs.
162+
- **`__mocks__/platform-bible-utils.ts`** — Stubs util functions including `isPlatformError`.
112163
- **`__mocks__/styleInlineMock.ts`** and **`__mocks__/styleMock.ts`** — Stub `.scss?inline` and `.scss`.
113164
- **`__mocks__/web-view-inline.ts`** — Stubs `*.web-view?inline` imports as a null-returning React component.
114165
- **`src/__tests__/test-helpers.ts`** — Exports `createTestActivationContext()` for testing `activate()` without type assertions.

src/__tests__/components/ContinuousView.test.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,15 @@ function makeBook(overrides?: Partial<Book>): Book {
5555
baselineText: 'In the',
5656
tokens: [
5757
{
58-
id: 'tok-0',
58+
ref: 'tok-0',
5959
surfaceText: 'In',
6060
writingSystem: 'en',
6161
type: 'word',
6262
charStart: 0,
6363
charEnd: 2,
6464
},
6565
{
66-
id: 'tok-1',
66+
ref: 'tok-1',
6767
surfaceText: 'the',
6868
writingSystem: 'en',
6969
type: 'word',
@@ -79,15 +79,15 @@ function makeBook(overrides?: Partial<Book>): Book {
7979
baselineText: 'beginning God',
8080
tokens: [
8181
{
82-
id: 'tok-2',
82+
ref: 'tok-2',
8383
surfaceText: 'beginning',
8484
writingSystem: 'en',
8585
type: 'word',
8686
charStart: 0,
8787
charEnd: 9,
8888
},
8989
{
90-
id: 'tok-3',
90+
ref: 'tok-3',
9191
surfaceText: 'God',
9292
writingSystem: 'en',
9393
type: 'word',
@@ -120,7 +120,7 @@ function makeTwoChapterBook(): Book {
120120
baselineText: 'Alpha',
121121
tokens: [
122122
{
123-
id: 'ch1-tok-0',
123+
ref: 'ch1-tok-0',
124124
surfaceText: 'Alpha',
125125
writingSystem: 'en',
126126
type: 'word',
@@ -136,7 +136,7 @@ function makeTwoChapterBook(): Book {
136136
baselineText: 'Beta',
137137
tokens: [
138138
{
139-
id: 'ch2-tok-0',
139+
ref: 'ch2-tok-0',
140140
surfaceText: 'Beta',
141141
writingSystem: 'en',
142142
type: 'word',
@@ -168,7 +168,7 @@ function makeSingleTokenBook(): Book {
168168
baselineText: 'Word',
169169
tokens: [
170170
{
171-
id: 'tok-only',
171+
ref: 'tok-only',
172172
surfaceText: 'Word',
173173
writingSystem: 'en',
174174
type: 'word',
@@ -199,7 +199,7 @@ function makeMixedBook(): Book {
199199
baselineText: 'In the',
200200
tokens: [
201201
{
202-
id: 'mix-tok-0',
202+
ref: 'mix-tok-0',
203203
surfaceText: 'In',
204204
writingSystem: 'en',
205205
type: 'word',
@@ -215,7 +215,7 @@ function makeMixedBook(): Book {
215215
baselineText: '.',
216216
tokens: [
217217
{
218-
id: 'mix-punct-0',
218+
ref: 'mix-punct-0',
219219
surfaceText: '.',
220220
writingSystem: 'en',
221221
type: 'punctuation',
@@ -247,7 +247,7 @@ function makeWordFreeBook(): Book {
247247
baselineText: '...',
248248
tokens: [
249249
{
250-
id: 'wf-punct-0',
250+
ref: 'wf-punct-0',
251251
surfaceText: '.',
252252
writingSystem: 'en',
253253
type: 'punctuation',

0 commit comments

Comments
 (0)