Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
c33d1a0
Add interlinearizer project storage and createProject command
alex-rawlings-yyc Apr 29, 2026
57b852e
Documentation improvement
alex-rawlings-yyc May 5, 2026
b7475ef
Documentation improvements, improve error handling when deleting proj…
alex-rawlings-yyc May 5, 2026
427b4ca
Set `restoreMocks` to `true` in jest config
alex-rawlings-yyc May 5, 2026
a2c3a29
Make sure notification failure doesn't cause unnecessary throw, reord…
alex-rawlings-yyc May 5, 2026
2ccc35f
Prevent same-project selection when creating an interlinear project, …
alex-rawlings-yyc May 5, 2026
62d367e
Add missing `localizedStrings`, add clarifying comment, improve test …
alex-rawlings-yyc May 6, 2026
37cd369
Improve docstring coverage
alex-rawlings-yyc May 6, 2026
428a925
Add project creation, metadata editing, and project selection UI
alex-rawlings-yyc May 7, 2026
dd17954
Fix modal close/callback ordering and trim whitespace from language i…
alex-rawlings-yyc May 7, 2026
2b8cd52
Return full project JSON from createProject and add error handling fo…
alex-rawlings-yyc May 7, 2026
fc65dcb
Use `<dialog>` for modals, guard on falsy update return, and relax gl…
alex-rawlings-yyc May 7, 2026
446df00
Fix analysis language default, button state, and storage error propag…
alex-rawlings-yyc May 7, 2026
1cc8e7a
Throw on malformed createProject response instead of silently skippin…
alex-rawlings-yyc May 7, 2026
9cbcc7c
Cleanup after incredibly messy rebase
alex-rawlings-yyc May 11, 2026
35b343e
More post-rebase cleanup
alex-rawlings-yyc May 11, 2026
3987f06
Improve command registration usage and sanity
alex-rawlings-yyc May 11, 2026
98bfeb6
Disable cancel during submission to avoid "I cancelled but the projec…
alex-rawlings-yyc May 12, 2026
734a8d0
Move `projectStorage` to `services` directory
alex-rawlings-yyc May 12, 2026
8f94abc
Improve project creation/update error handling, improve project load …
alex-rawlings-yyc May 12, 2026
15aac72
Improve `projectStorage` error-handling and documentation
alex-rawlings-yyc May 12, 2026
9bafd20
Add missing `\@throws` docstring
alex-rawlings-yyc May 12, 2026
07775b4
Rename modal control commands for clarity, fix handlers in Interlinea…
alex-rawlings-yyc May 12, 2026
bfbde3e
Improve docs/schema description
alex-rawlings-yyc May 12, 2026
69aeed4
Split modals into separate component (#62)
imnasnainaec May 13, 2026
7698c2d
Remove TOCTOU race from PAPI storage writes, remove duplicate notific…
alex-rawlings-yyc May 13, 2026
cb42224
Add eslint ignore, add submitting ref
alex-rawlings-yyc May 13, 2026
0e5ab85
Add missing docs
alex-rawlings-yyc May 13, 2026
c62d0f7
Send notification when created project has unexpected shape, add subm…
alex-rawlings-yyc May 13, 2026
2a15e91
Validate name and description in type guard
alex-rawlings-yyc May 13, 2026
aa156f6
Prevent project write race, add aria prop
alex-rawlings-yyc May 13, 2026
34c7612
Disable buttons while submitting/loading, update docs, make implicit …
alex-rawlings-yyc May 13, 2026
180ef23
Skip and log singleton corrupted project records
alex-rawlings-yyc May 13, 2026
c083626
Update remaining `tw-` tags
alex-rawlings-yyc May 14, 2026
799061f
Extract modal logic into ProjectModals, tighten type guards and coverage
alex-rawlings-yyc May 14, 2026
0920f36
Update docs, fix misleading test
alex-rawlings-yyc May 14, 2026
dd43094
Align `atStart` and `atEnd` length checks, get rid of `createSourceIs…
alex-rawlings-yyc May 15, 2026
80a5a38
Fix and consolidate modal styles (#74)
imnasnainaec May 15, 2026
af58f61
Simplify interlinear model (#63)
alex-rawlings-yyc May 15, 2026
6ab1ee0
Align code with simplified interlinear model
alex-rawlings-yyc May 15, 2026
5d59694
Memoize Modal handler callbacks, add `targetProjectId` to modals wher…
alex-rawlings-yyc May 15, 2026
014353f
Add missing JSDocs
alex-rawlings-yyc May 15, 2026
8a08ee0
Align stub with actual implementation, send notification when save/su…
alex-rawlings-yyc May 15, 2026
3eafb2b
Trimmed down `AGENTS.md` to where it was before post-model-change update
alex-rawlings-yyc May 15, 2026
eb39965
Prevent stale data race condition, update old TW classes, TW class co…
alex-rawlings-yyc May 15, 2026
1d2525f
Prevent double notifications
alex-rawlings-yyc May 15, 2026
54d9076
Improve docs, minor fixes
alex-rawlings-yyc May 15, 2026
3a0d21c
Add aria modal tag where missing
alex-rawlings-yyc May 19, 2026
a9bb8a7
Post-rebase cleanup
alex-rawlings-yyc May 19, 2026
b0f3335
Make `webViewNonce` optional, revert to earlier TW classes, introduce…
alex-rawlings-yyc May 20, 2026
069d4e3
Fix lint error
alex-rawlings-yyc May 20, 2026
5842232
Fix Tailwind classes, clean up test files, ignore branches that don't…
alex-rawlings-yyc May 21, 2026
8e2278b
Fix lint issue
alex-rawlings-yyc May 21, 2026
6a38543
Fix test failure
alex-rawlings-yyc May 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 28 additions & 14 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,20 @@ This is a **Platform.Bible extension** for interlinear Bible text alignment. Pla

### Extension entry point

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

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

`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.

### WebView UI

`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:`).
[src/interlinearizer.web-view.tsx](src/interlinearizer.web-view.tsx) — entry point rendered inside Platform.Bible's WebView iframe; 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).

[InterlinearizerLoader](src/components/InterlinearizerLoader.tsx) — real top of the React tree: owns modal state, persists the active interlinear project, fetches and tokenizes book data, and routes top-menu commands to the appropriate modal.

[Interlinearizer](src/components/Interlinearizer.tsx) — renders the interlinear view from the loaded book data.

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

Expand All @@ -47,12 +51,20 @@ import interlinearizerReact from './interlinearizer.web-view?inline';
import interlinearizerStyles from './interlinearizer.web-view.scss?inline';
```

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

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.

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.

### Project modals

[src/components/ProjectModals.tsx](src/components/ProjectModals.tsx) — single mount point for all project-related dialogs, switching between `'select' | 'create' | 'metadata' | 'none'` states. The three modal components ([SelectInterlinearProjectModal](src/components/SelectInterlinearProjectModal.tsx), [CreateProjectModal](src/components/CreateProjectModal.tsx), [ProjectMetadataModal](src/components/ProjectMetadataModal.tsx)) call backend commands to list, create, update, and delete projects.

### Project storage

[src/services/projectStorage.ts](src/services/projectStorage.ts) — owns all `papi.storage` reads and writes for interlinearizer projects. Two serialization queues prevent interleaved read-modify-write races. Tests must call `resetQueuesForTesting()` between tests because module state is not cleared by `resetMocks`.

### Styling

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`.
Expand All @@ -61,23 +73,24 @@ All UI uses Tailwind CSS (via `src/tailwind.css`). Every Tailwind class is prefi

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

1. `src/parsers/papi/usjBookExtractor.ts` — converts USJ to the internal `Book` type
2. `src/parsers/papi/bookTokenizer.ts` — segments and tokenizes the book into `Segment`/`Token` structures with character offsets
1. [src/parsers/papi/usjBookExtractor.ts](src/parsers/papi/usjBookExtractor.ts) — converts USJ to the internal `RawBook` type
2. [src/parsers/papi/bookTokenizer.ts](src/parsers/papi/bookTokenizer.ts) — segments and tokenizes the book into `Segment`/`Token` structures with character offsets

`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`.
[src/parsers/pt9/interlinearXmlParser.ts](src/parsers/pt9/interlinearXmlParser.ts) — separately parses Paratext 9 interlinear XML into the alignment model. The XML schema is documented in [pt9-xml.md](src/parsers/pt9/pt9-xml.md).

### Data model (`src/types/interlinearizer.d.ts`)
### Data model ([src/types/interlinearizer.d.ts](src/types/interlinearizer.d.ts))

The core types are:

- `InterlinearAlignment` — top-level bilingual container (source + target `InterlinearText`)
- `InterlinearProject` — persisted envelope: id, createdAt, optional name/description, `sourceProjectId`, optional `targetProjectId`, `analysisLanguages`, `analysis: TextAnalysis`, and optional `links`. Only this is serialized to storage; the `Book` hierarchy is rebuilt from USJ on each load.
- `ActiveProject` — runtime pairing of `project: InterlinearProject` with reconstructed `source` and optional `target` books.
- `Book → Segment → Token` — the text hierarchy
- `TextAnalysis` — flat analysis layer keyed by id (does **not** mirror text hierarchy)
- `TokenAnalysis / Morpheme` — parse and 1:1 glosses; multiple analyses per token are allowed, distinguished by `status`
- `AlignmentLink` — links between source and target tokens/morphemes
- `AlignmentEndpoint` — has either token-level or morpheme-level specificity, never both
- `TokenAnalysis / MorphemeAnalysis` — parse and 1:1 glosses; multiple analyses per token are allowed, distinguished by `status`
- `AlignmentLink` — directional links between source and target endpoints
- `AlignmentEndpoint` — has either token-level or morpheme-level specificity

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.
Key invariants: `Segment.baselineText.slice(charStart, charEnd) === Token.surfaceText`; at most one linked analysis per token/segment may have `status: 'approved'`. `MultiString` values are keyed by BCP 47 tags. `TokenSnapshot.surfaceText` detects drift when baseline text changes.

### TypeScript path aliases

Expand All @@ -103,9 +116,10 @@ Key semantic properties of the mock setup:
Mock files:

- **`__mocks__/fileMock.ts`** — Stub static asset imports.
- **`__mocks__/lucide-react.tsx`** — Stubs icon components used in modals.
- **`__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.
- **`__mocks__/papi-core.ts`** — Empty module; exists only for module resolution since `@papi/core` is types-only at runtime.
- **`__mocks__/papi-frontend.ts`** — Stubs `logger` (debug/error/info/warn as jest fns).
- **`__mocks__/papi-frontend.ts`** — Stubs `logger` (debug/error/info/warn as jest fns) and `papi.commands.sendCommand` / `papi.notifications.send`.
- **`__mocks__/papi-frontend-react.ts`** — Stubs PAPI React hooks.
- **`__mocks__/platform-bible-react.tsx`** — Stubs components with appropriate `data-testid` attributes. See file for test IDs.
- **`__mocks__/platform-bible-utils.ts`** — Stubs util functions.
Expand Down
25 changes: 25 additions & 0 deletions __mocks__/lucide-react.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* @file Jest mock for lucide-react. Provides stub icon components used by extension components.
*/

import type { ReactElement } from 'react';

/**
* Stub for the Trash2 icon; renders a bare SVG element so tests can locate the icon by test ID.
*
* @param props - SVG props forwarded from the component, including optional className and size.
* @returns A ReactElement SVG element used as a trash icon stub in tests.
*/
export function Trash2(props: Readonly<{ className?: string; size?: number }>): ReactElement {
return <svg data-testid="trash-icon" {...props} />;
}

/**
* Stub for the Info icon; renders a bare SVG element so tests can locate the icon by test ID.
*
* @param props - SVG props forwarded from the component, including optional className and size.
* @returns A ReactElement SVG element used as an info icon stub in tests.
*/
export function Info(props: Readonly<{ className?: string; size?: number }>): ReactElement {
return <svg data-testid="info-icon" {...props} />;
}
16 changes: 16 additions & 0 deletions __mocks__/papi-backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ const mockGetOpenWebViewDefinition = jest.fn();
const mockOnDidOpenWebView = jest.fn();
const mockOnDidCloseWebView = jest.fn();
const mockRegisterValidator = jest.fn();
const mockReadUserData = jest.fn();
const mockWriteUserData = jest.fn();
const mockDeleteUserData = jest.fn();
const mockNotificationsSend = jest.fn();
const mockLogger = {
debug: jest.fn(),
error: jest.fn(),
Expand All @@ -28,6 +32,14 @@ const papi = {
projectSettings: {
registerValidator: mockRegisterValidator,
},
notifications: {
send: mockNotificationsSend,
},
storage: {
readUserData: mockReadUserData,
writeUserData: mockWriteUserData,
deleteUserData: mockDeleteUserData,
},
webViewProviders: {
registerWebViewProvider: mockRegisterWebViewProvider,
},
Expand All @@ -49,6 +61,10 @@ const defaultExport = {
__mockOnDidOpenWebView: mockOnDidOpenWebView,
__mockOnDidCloseWebView: mockOnDidCloseWebView,
__mockRegisterValidator: mockRegisterValidator,
__mockReadUserData: mockReadUserData,
__mockWriteUserData: mockWriteUserData,
__mockDeleteUserData: mockDeleteUserData,
__mockNotificationsSend: mockNotificationsSend,
__mockLogger: mockLogger,
};

Expand Down
15 changes: 15 additions & 0 deletions __mocks__/papi-frontend-react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,28 @@ const useRecentScriptureRefs = jest
.fn()
.mockImplementation(() => ({ recentScriptureRefs: [], addRecentScriptureRef: jest.fn() }));

/**
* Mock for `useData`. Returns a `Proxy` whose property accesses each yield a function returning
* `[undefined, jest.fn(), false]`, matching the real hook's `[data, setter, isLoading]` tuple
* without requiring a live data provider.
*/
const useData = jest.fn(() =>
new Proxy(
{},
{
get: () => jest.fn().mockReturnValue([undefined, jest.fn(), false]),
},
),
);

module.exports = {
__esModule: true,
useProjectData,
useProjectSetting,
useSetting,
useLocalizedStrings,
useRecentScriptureRefs,
useData,
};

/** Marks this file as a module so top-level const/let are module-scoped. */
Expand Down
20 changes: 18 additions & 2 deletions __mocks__/papi-frontend.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* @file Jest mock for @papi/frontend. Provides a logger stub so WebView/frontend code can be
* unit-tested without loading the real Platform API.
* @file Jest mock for @papi/frontend. Provides a logger stub and a minimal papi object so
* WebView/frontend components can be unit-tested without loading the real Platform API.
*/

const mockLogger = {
Expand All @@ -10,8 +10,24 @@ const mockLogger = {
warn: jest.fn(),
};

const mockSendCommand = jest.fn();
const mockNotificationsSend = jest.fn();

const papi = {
commands: {
sendCommand: mockSendCommand,
},
notifications: {
send: mockNotificationsSend,
},
menuData: {
dataProviderName: 'platform.menuDataServiceDataProvider',
},
};

module.exports = {
__esModule: true,
default: papi,
logger: mockLogger,
};

Expand Down
Loading
Loading