Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
951a783
Add USJ book parsing pipeline and interlinearizer display WebView
alex-rawlings-yyc Apr 23, 2026
2392764
Fix lint error
alex-rawlings-yyc Apr 24, 2026
a2e7552
Add div for BookChapterControl, display entire chapters at once
alex-rawlings-yyc Apr 24, 2026
c58467f
Fix USJ chapter/verse traversal, surrogate-pair tokenization, and mem…
alex-rawlings-yyc Apr 24, 2026
45fed16
Fix tokenizer edge cases, para double-space, and stableStringify loca…
alex-rawlings-yyc Apr 24, 2026
df0cc4c
Fix mock function signatures, remove orphaned mocks
alex-rawlings-yyc Apr 24, 2026
a590bde
Fix useMemo deps, improve error logging, and clarify test/doc wording
alex-rawlings-yyc Apr 24, 2026
1c13ef1
Improve error display, heading-para skipping, and test/code tidying
alex-rawlings-yyc Apr 24, 2026
2076fd8
Fix correctness bugs and tighten tests from code review
alex-rawlings-yyc Apr 24, 2026
37e78bf
Fix stableStringify undefined handling and add test
alex-rawlings-yyc Apr 24, 2026
2dc4e72
Documentation/Error type audit
alex-rawlings-yyc Apr 24, 2026
b12f00f
Clean-up incomplete changes (#32)
imnasnainaec Apr 27, 2026
5598f71
Fix CodeRabbit nitpicks
alex-rawlings-yyc Apr 27, 2026
1f4cde5
Fix nitpicks, revert previous nitpick
alex-rawlings-yyc Apr 27, 2026
2024a89
Improve docstring coverage
alex-rawlings-yyc Apr 27, 2026
5e69344
Fix div-in-button issue, reintroduce export for backend mock to ensur…
alex-rawlings-yyc Apr 27, 2026
e5ed09c
Improve parsing of index and length in pt9 XML Parser, reject duplica…
alex-rawlings-yyc Apr 27, 2026
46ab515
Tighten range/index validation to exclude non-whole numbers
alex-rawlings-yyc Apr 27, 2026
477ec4f
Add TabToolbar to WebView, audit mocks
alex-rawlings-yyc Apr 28, 2026
dc4d30d
Improve error logging, assert writing system fallback test, improve mock
alex-rawlings-yyc Apr 28, 2026
55716a1
Fix WebView registration command result schema, improved description …
alex-rawlings-yyc Apr 29, 2026
8c5d31b
Addressing Danny's notes
alex-rawlings-yyc Apr 29, 2026
accb3d8
Addressing CR nitpicks
alex-rawlings-yyc Apr 29, 2026
4c7bb09
Improve token regexes, minor adjustments, add docstring to `parseStri…
alex-rawlings-yyc Apr 29, 2026
da05e57
Improve `parseStrictNumber()`, improve hook mock in `papi-frontend-re…
alex-rawlings-yyc Apr 29, 2026
5fc175c
Expand coverage collection to all source files
alex-rawlings-yyc Apr 29, 2026
c6d91cb
Clean up regex (#34)
imnasnainaec Apr 29, 2026
f391011
Clarify `SyntaxError` when `Range` from `Cluster` is invalid, improve…
alex-rawlings-yyc Apr 29, 2026
494d322
Revert `openInterlinearizerForWebView`, as Platform.Bible has a proje…
alex-rawlings-yyc Apr 29, 2026
2aff42b
Allow word-final apostrophe glottal markers to be captured
alex-rawlings-yyc Apr 29, 2026
13acc79
Update comment
alex-rawlings-yyc Apr 29, 2026
503883c
Tweak tests
imnasnainaec Apr 30, 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ The general file structure for an extension is as follows:
- `src/` contains the source code for the extension
- `src/main.ts` is the main entry file for the extension (registers commands and wires interlinear XML)
- `src/types/interlinearizer.d.ts` is this extension's types file that defines how other extensions can use this extension through the `papi`. It is copied into the build folder
- `src/parsers/interlinearXmlParser.ts` parses interlinear XML into structured data (uses fast-xml-parser). The PT9 XML schema and parsed output are documented in `src/parsers/pt9-xml.md`
- `src/parsers/pt9/` contains parser and schema for parsing PT9 interlinear XML into structured data
- `*.web-view.tsx` files will be treated as React WebViews
- `*.web-view.scss` files provide styles for WebViews
- `*.web-view.html` files are a conventional way to provide HTML WebViews (no special functionality)
Expand Down
9 changes: 0 additions & 9 deletions __mocks__/interlinearXmlContent.ts

This file was deleted.

8 changes: 0 additions & 8 deletions __mocks__/papi-backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,6 @@ module.exports = {
__esModule: true,
default: defaultExport,
logger: mockLogger,
__mockRegisterWebViewProvider: mockRegisterWebViewProvider,
__mockRegisterCommand: mockRegisterCommand,
__mockOpenWebView: mockOpenWebView,
__mockSelectProject: mockSelectProject,
__mockGetOpenWebViewDefinition: mockGetOpenWebViewDefinition,
__mockOnDidOpenWebView: mockOnDidOpenWebView,
__mockOnDidCloseWebView: mockOnDidCloseWebView,
__mockLogger: mockLogger,
};

/** Marks this file as a module so top-level const/let are module-scoped; avoids TS "redeclare" when both papi-backend and papi-frontend mocks are in the project (they are used mutually exclusively by Jest). */
Expand Down
93 changes: 50 additions & 43 deletions __mocks__/papi-frontend-react.ts
Original file line number Diff line number Diff line change
@@ -1,63 +1,70 @@
/**
* @file Jest mock for @papi/frontend/react. Provides stub implementations of various PAPI React hooks so
* @file Jest mock for @papi/frontend/react. Provides stub implementations of PAPI React hooks so
* WebView/frontend components can be unit-tested without the real Platform API.
*/

/**
* useData('providerName') returns an object whose keys are data type names and values are hooks.
* Mock: any property returns a function that returns [undefined, setter, false].
* Mock for `useProjectData`. 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 createUseDataLikeHook = () =>
jest.fn(() =>
new Proxy(
{},
{
get: () => () => [undefined, jest.fn(), false],
},
),
);
const useProjectData = jest.fn(() =>
new Proxy(
{},
{
get: () => () => [undefined, jest.fn(), false],
},
),
);

const useDataProvider = jest.fn().mockReturnValue(undefined);
const useData = createUseDataLikeHook();
const useScrollGroupScrRef = jest.fn().mockReturnValue([undefined, jest.fn()]);
const useSetting = jest.fn().mockImplementation((_key: string, defaultState: unknown) => [defaultState, jest.fn()]);
const useProjectData = createUseDataLikeHook();
const useProjectDataProvider = jest.fn().mockReturnValue(undefined);
/**
* Mock for `useProjectSetting`. Returns `[defaultState, setSetting, resetSetting, isLoading]`,
* passing `defaultState` through unchanged so callers receive a predictable initial value.
*
* @param _projectDataProviderSource - Ignored project data provider source.
* @param _key - Ignored setting key.
* @param defaultState - Value surfaced as the current setting state.
* @returns Tuple of `[defaultState, jest.fn(), jest.fn(), false]`.
*/
const useProjectSetting = jest
.fn()
.mockImplementation((_projectInterface: string, _projectIdOrPdp: unknown, _key: string, defaultState: unknown) => [
.mockImplementation((_projectDataProviderSource: unknown, _key: string, defaultState: unknown) => [
defaultState,
jest.fn(),
jest.fn(),
false,
]);
const useDialogCallback = jest.fn().mockReturnValue(jest.fn());
const useDataProviderMulti = jest.fn().mockReturnValue([]);
/** Returns a map of localization key -> key (so tests get a string for each key). */
const useLocalizedStrings = jest.fn().mockImplementation((keys: string[]) =>
Array.isArray(keys) ? keys.reduce<Record<string, string>>((acc, k) => ({ ...acc, [k]: k }), {}) : {},
);
const useWebViewController = jest.fn().mockReturnValue(undefined);
const useRecentScriptureRefs = jest.fn().mockReturnValue([]);

/**
* Mock for `useLocalizedStrings`. Maps each requested key to itself so tests receive a
* predictable `Record<string, string>` without a real localization service.
*
* @param keys - BCP47-style string keys to resolve.
* @returns Tuple of `[record, isLoading]` where every key maps to itself and `isLoading` is
* `false`.
*/
const useLocalizedStrings = jest.fn().mockImplementation((keys: string[]) => [
Array.isArray(keys) ? keys.reduce<Record<string, string>>((acc, k) => { acc[k] = k; return acc; }, {}) : {},
false,
]);

/**
* Mock for `useRecentScriptureRefs`. Returns an empty history and a no-op `addRecentScriptureRef`
* so components that display recent references render without errors.
*
* @returns Object with `recentScriptureRefs` (empty array) and `addRecentScriptureRef` (jest spy).
*/
const useRecentScriptureRefs = jest
.fn()
.mockImplementation(() => ({ recentScriptureRefs: [], addRecentScriptureRef: jest.fn() }));

module.exports = {
__esModule: true,
useDataProvider,
useData,
useScrollGroupScrRef,
useSetting,
useProjectData,
useProjectDataProvider,
useProjectSetting,
useDialogCallback,
useDataProviderMulti,
useLocalizedStrings,
useWebViewController,
useRecentScriptureRefs,
__mockUseDataProvider: useDataProvider,
__mockUseData: useData,
__mockUseLocalizedStrings: useLocalizedStrings,
__mockUseSetting: useSetting,
__mockUseProjectData: useProjectData,
__mockUseProjectDataProvider: useProjectDataProvider,
__mockUseProjectSetting: useProjectSetting,
__mockUseWebViewController: useWebViewController,
};

/** Marks this file as a module so top-level const/let are module-scoped. */
export {};
47 changes: 3 additions & 44 deletions __mocks__/papi-frontend.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/**
* @file Jest mock for @papi/frontend. Provides papi, logger, network, projectDataProviders, and other
* renderer API stubs so WebView/frontend code can be unit-tested without loading the real
* Platform API.
* @file Jest mock for @papi/frontend. Provides a logger stub so WebView/frontend code can be
* unit-tested without loading the real Platform API.
*/

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

const mockNetwork = {
request: jest.fn(),
subscribe: jest.fn().mockReturnValue({ dispose: jest.fn() }),
};

const mockProjectDataProviders = {
get: jest.fn().mockResolvedValue(undefined),
register: jest.fn().mockResolvedValue({ dispose: jest.fn() }),
};

const mockWebViews = {
getWebView: jest.fn(),
openWebView: jest.fn().mockResolvedValue(undefined),
};

/** Default papi object shape used in renderer/WebViews. Only commonly used services are stubbed. */
const papi = {
logger: mockLogger,
network: mockNetwork,
projectDataProviders: mockProjectDataProviders,
webViews: mockWebViews,
react: {}, // Re-export of @papi/frontend/react; tests usually import that module directly.
};

const defaultExport = {
...papi,
__mockLogger: mockLogger,
__mockNetwork: mockNetwork,
__mockProjectDataProviders: mockProjectDataProviders,
__mockWebViews: mockWebViews,
};

module.exports = {
__esModule: true,
default: defaultExport,
logger: mockLogger,
network: mockNetwork,
projectDataProviders: mockProjectDataProviders,
webViews: mockWebViews,
__mockLogger: mockLogger,
__mockNetwork: mockNetwork,
__mockProjectDataProviders: mockProjectDataProviders,
__mockWebViews: mockWebViews,
};

/** Marks this file as a module so top-level const/let are module-scoped; avoids TS "redeclare" when both papi-backend and papi-frontend mocks are in the project (they are used mutually exclusively by Jest). */
/** Marks this file as a module so top-level const/let are module-scoped. */
export {};
84 changes: 84 additions & 0 deletions __mocks__/platform-bible-react.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* @file Jest mock for platform-bible-react. The real package ships ESM which Jest cannot parse
* without extra transform configuration. This stub provides the subset used by extension
* components: `BookChapterControl`, `BOOK_CHAPTER_CONTROL_STRING_KEYS`, `TabToolbar`, and
* `ScrollGroupSelector`.
*/

import type { ReactElement, ReactNode } from 'react';

interface SerializedVerseRef {
book: string;
chapterNum: number;
verseNum: number;
verse?: string;
versificationStr?: string;
}

export const BOOK_CHAPTER_CONTROL_STRING_KEYS: string[] = [];

export function TabToolbar({
startAreaChildren,
endAreaChildren,
}: Readonly<{
className?: string;
startAreaChildren?: ReactNode;
endAreaChildren?: ReactNode;
onSelectProjectMenuItem?: () => void;
onSelectViewInfoMenuItem?: () => void;
}>): ReactElement {
return (
<div data-testid="tab-toolbar">
<div data-testid="tab-toolbar-start">{startAreaChildren}</div>
<div data-testid="tab-toolbar-end">{endAreaChildren}</div>
</div>
);
}

export function ScrollGroupSelector({
availableScrollGroupIds,
scrollGroupId,
onChangeScrollGroupId,
}: Readonly<{
availableScrollGroupIds?: (number | undefined)[];
scrollGroupId?: number;
onChangeScrollGroupId?: (id: number | undefined) => void;
}>): ReactElement {
return (
<select
data-testid="scroll-group-selector"
value={scrollGroupId ?? ''}
onChange={(e) => onChangeScrollGroupId?.(e.target.value === '' ? undefined : Number(e.target.value))}
>
<option value="">—</option>
{availableScrollGroupIds?.map((id) => (
<option key={id ?? 'undefined'} value={id ?? ''}>
{id ?? '—'}
</option>
))}
</select>
);
}

export function BookChapterControl({
scrRef,
handleSubmit,
onAddRecentSearch,
}: Readonly<{
scrRef: SerializedVerseRef;
handleSubmit: (ref: SerializedVerseRef) => void;
className?: string;
localizedStrings?: Record<string, string>;
recentSearches?: SerializedVerseRef[];
onAddRecentSearch?: (scrRef: SerializedVerseRef) => void;
id?: string;
}>): ReactElement {
return (
<div data-testid="book-chapter-control">
{scrRef.book} {scrRef.chapterNum}:{scrRef.verseNum}
<button type="button" onClick={() => {handleSubmit(scrRef); onAddRecentSearch?.(scrRef);}}>
Submit reference
</button>
</div>
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
11 changes: 7 additions & 4 deletions __mocks__/platform-bible-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class UnsubscriberAsyncList {
(unsubscriber as { dispose: UnsubscriberFn }).dispose.bind(unsubscriber)
);
} else if (typeof unsubscriber === 'function') {
this.unsubscribers.add(unsubscriber as UnsubscriberFn);
this.unsubscribers.add(unsubscriber);
}
});
}
Expand All @@ -53,13 +53,16 @@ class UnsubscriberAsyncList {
}
}

/** Minimal PlatformError shape matching the real platform-bible-utils type. */
/**
* Minimal PlatformError shape matching the real platform-bible-utils type. Uses `platformErrorVersion`
* as the discriminant — the same field the real `isPlatformError` checks.
*/
interface PlatformError {
message: string;
isPlatformError: true;
platformErrorVersion: number;
}

const isPlatformError = (value: unknown): value is PlatformError =>
typeof value === 'object' && value !== null && (value as PlatformError).isPlatformError === true;
typeof value === 'object' && value !== null && 'platformErrorVersion' in (value);

export { UnsubscriberAsyncList, isPlatformError };
4 changes: 4 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@
"eflomal",
"electronmon",
"endregion",
"eten",
"finalizer",
"Fragmenter",
"guids",
"hopkinson",
"iframes",
"imte",
"interlinearization",
"interlinearizer",
"localstorage",
Expand All @@ -44,6 +46,8 @@
"pdps",
"plusplus",
"proxied",
"Punct",
"recalc",
"reinitializing",
"reserialized",
"scriptio",
Expand Down
15 changes: 5 additions & 10 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,10 @@ const config: Config = {
*/
collectCoverage: false,

/**
* Collect coverage from parsers, main entry (main.ts), and WebView UI
* (interlinearizer.web-view.tsx). Excludes test files, type declarations, and build artifacts.
*/
/** Collect coverage from all source files. Excludes type declarations and test files. */
collectCoverageFrom: [
'src/parsers/**/*.ts',
'src/main.ts',
'src/**/*.web-view.tsx',
'!src/parsers/**/*.d.ts',
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/__tests__/**',
'!src/**/*.test.{ts,tsx}',
'!src/**/*.spec.{ts,tsx}',
Expand Down Expand Up @@ -92,12 +87,12 @@ const config: Config = {
'^@papi/frontend/react$': '<rootDir>/__mocks__/papi-frontend-react.ts',
/** Mock so test-helpers get UnsubscriberAsyncList without loading ESM deps. */
'^platform-bible-utils$': '<rootDir>/__mocks__/platform-bible-utils.ts',
/** Mock ESM deps that Jest cannot parse. */
'^platform-bible-react$': '<rootDir>/__mocks__/platform-bible-react.tsx',
/** Resolve webpack ?inline imports. */
'^(.+)\\.web-view\\?inline$': '<rootDir>/__mocks__/web-view-inline.ts',
/** Resolve webpack ?inline imports: SCSS content. */
'^(.+)\\.(scss|sass|css)\\?inline$': '<rootDir>/__mocks__/styleInlineMock.ts',
/** Resolve webpack ?raw import for test XML in web-view. */
'^(.+)/Interlinear_en_MAT\\.xml\\?raw$': '<rootDir>/__mocks__/interlinearXmlContent.ts',
},

/** Exclude dist from module resolution to avoid Haste naming collision with root package.json. */
Expand Down
Loading
Loading