Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fad023b
Add interlinearizer feature with XML parsing and web view integration
alex-rawlings-yyc Feb 2, 2026
b89be37
Merge remote-tracking branch 'origin/main' into xml-parser
alex-rawlings-yyc Feb 2, 2026
c045770
Update package dependencies and enhance README documentation
alex-rawlings-yyc Feb 3, 2026
229f51a
Update src/main.ts
alex-rawlings-yyc Feb 3, 2026
3ce4ae8
Refactor interlinearizer imports and update main web view type
alex-rawlings-yyc Feb 3, 2026
45740d2
Update import statement in interlinearXmlParser.ts to use type imports
alex-rawlings-yyc Feb 3, 2026
1811aa4
Refactor interlinearXmlParser.ts to ensure index and length attribute…
alex-rawlings-yyc Feb 4, 2026
e7053e8
Add Jest configuration and unit tests for interlinearizer feature
alex-rawlings-yyc Feb 5, 2026
bb1a4a3
Update Jest coverage threshold to enforce 100% coverage on parsers, m…
alex-rawlings-yyc Feb 5, 2026
dab283d
Enhance README and Jest mocks for improved testing clarity
alex-rawlings-yyc Feb 5, 2026
8fc0836
Update src/types/interlinearizer.d.ts
alex-rawlings-yyc Feb 5, 2026
f962b14
Enhance configuration and update tests for improved clarity and funct…
Feb 6, 2026
07ea134
Update README formatting
Feb 6, 2026
596d95a
Refactor path resolution and enhance tests for clarity
Feb 7, 2026
e6bcd47
Enhance Jest configuration and update README for clarity
Feb 10, 2026
410e203
Refactor Jest configuration and enhance error handling in web-view co…
Feb 10, 2026
658791f
Add interlinear XML schema documentation to README
Feb 10, 2026
2d38026
Update Jest mock file comments to include @file annotations for clarity
alex-rawlings-yyc Feb 11, 2026
764f583
Reintroduced 'Excluded' flag in interlinear XML parsing and update do…
Feb 12, 2026
c3dcfc8
Fix numeric hash type handling
myieye Feb 13, 2026
4756ca6
Optimize xml-parser isArray
myieye Feb 13, 2026
c9c24aa
Enhance documentation for interlinear XML parsing
alex-rawlings-yyc Feb 13, 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
4 changes: 4 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ dist
release
temp-build

# Jest
__mocks__
coverage

# generated files
package-lock.json

Expand Down
14 changes: 13 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// #region shared with https://github.com/paranext/paranext-multi-extension-template/blob/main/.eslintrc.cjs

const path = require('path');

module.exports = {
extends: [
// https://github.com/electron-react-boilerplate/eslint-config-erb/blob/main/index.js
Expand Down Expand Up @@ -155,6 +157,14 @@ module.exports = {
'import/no-self-import': 'off',
},
},
{
// Jest globals (describe, it, expect, etc.) so ESLint does not report no-undef
files: ['**/*.test.{ts,tsx}', '**/*.spec.{ts,tsx}', '**/__tests__/**/*.{ts,tsx}'],
plugins: ['jest'],
env: {
'jest/globals': true,
},
},
],
parserOptions: {
ecmaVersion: 2022,
Expand All @@ -163,11 +173,13 @@ module.exports = {
tsconfigRootDir: __dirname,
createDefaultProgram: true,
},
plugins: ['@typescript-eslint', 'no-type-assertion', 'no-null'],
plugins: ['@typescript-eslint', 'jest', 'no-type-assertion', 'no-null'],
settings: {
'import/resolver': {
typescript: {
alwaysTryTypes: true,
// Absolute path so path aliases (@main, parsers/*) resolve regardless of CWD or file location
project: path.join(__dirname, 'tsconfig.json'),
},
},
'import/parsers': {
Expand Down
49 changes: 49 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Test

on:
push:
branches: ['main', 'release-prep', 'hotfix-*']
pull_request:
branches: ['main', 'release-prep', 'hotfix-*']

permissions:
contents: read

jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
steps:
- name: Checkout git repo
uses: actions/checkout@v4
with:
path: extension-repo

- name: Checkout paranext-core repo to use its sub-packages
uses: actions/checkout@v4
with:
path: paranext-core
repository: paranext/paranext-core

- name: Setup Node.js
uses: actions/setup-node@v4
with:
cache: 'npm'
cache-dependency-path: |
extension-repo/package-lock.json
paranext-core/package-lock.json
node-version-file: extension-repo/package.json

- name: Install extension dependencies
working-directory: extension-repo
run: npm ci

- name: Install core dependencies
working-directory: paranext-core
run: npm ci --ignore-scripts

- name: Run tests
working-directory: extension-repo
run: npm test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ dist
release
dist-ssr
*.local
coverage

# formatting and linting
.eslintcache
Expand Down
4 changes: 4 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ dist
release
temp-build

# Jest
__mocks__
coverage

# generated files
package-lock.json

Expand Down
4 changes: 4 additions & 0 deletions .stylelintignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ dist
release
temp-build

# Jest
__mocks__
coverage

# generated files
package-lock.json

Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,21 +90,26 @@ Note: if you [update this extension from the template](#to-update-this-extension

The general file structure for an extension is as follows:

- `package.json` contains information about this extension's npm package. It is required for Platform.Bible to use the extension properly. It is copied into the build folder
- `package.json` (and `package-lock.json`) contain information about this extension's npm package and lockfile. They are required for Platform.Bible to use the extension properly. The lockfile is project-specific and is not synced from the template. The built extension is copied into the build folder
- `manifest.json` is the manifest file that defines the extension and important properties for Platform.Bible. It is copied into the build folder
- `src/` contains the source code for the extension
- `src/main.ts` is the main entry file 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`
- `*.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)
- `src/__tests__/` contains unit tests (Jest) for the extension, including parser tests (valid and invalid XML, edge cases) and web-view tests
- `__mocks__/` contains Jest mocks for the PAPI, file modules, and test fixtures used by tests in `src/__tests__/`. The `@papi/backend` and `@papi/frontend` mocks are used mutually exclusively (backend for main.ts tests, frontend for WebView tests); each mock file ends with `export {}` so TypeScript treats it as a module.
- `assets/` contains asset files the extension and its WebViews can retrieve using the `papi-extension:` protocol, as well as textual descriptions in various languages. It is copied into the build folder
- `assets/displayData.json` contains (optionally) a path to the extension's icon file as well as text for the extension's display name, short summary, and path to the full description file
- `assets/descriptions/` contains textual descriptions of the extension in various languages
- `assets/descriptions/description-<locale>.md` contains a brief description of the extension in the language specified by `<locale>`
- `contributions/` contains JSON files the platform uses to extend data structures for things like menus and settings. The JSON files are referenced from the manifest
- `public/` contains other static files that are copied into the build folder
- `test-data/` contains sample interlinear XML (e.g. `Interlinear_en_MAT.xml`) for development and tests
- `.github/` contains files to facilitate integration with GitHub
- `.github/workflows` contains [GitHub Actions](https://github.com/features/actions) workflows for automating various processes in this repo
- `.github/workflows` contains [GitHub Actions](https://github.com/features/actions) workflows for automating various processes in this repo (e.g. **Test** and **Lint** on push/PR to main, release-prep, hotfix-\*; **Publish** and **Bump Versions** manual dispatch; **CodeQL** for security)
- `.github/assets/release-body.md` combined with a generated changelog becomes the body of [releases published using GitHub Actions](#publishing)
- `dist/` is a generated folder containing the built extension files
- `release/` is a generated folder containing a zip of the built extension files
Expand Down Expand Up @@ -155,7 +160,6 @@ These steps will walk you through releasing a version on GitHub and bumping the
1. Make sure the versions in this repo are on the version number you want to release. If they are not, manually dispatch the [Bump Versions workflow](#bumping-version-without-publishing-a-release) or run the `bump-versions` npm script to set the versions to what you want to release on the branch you want to release from.

2. Manually dispatch the Publish workflow in GitHub Actions targeting the branch you want to release from. This workflow creates a new pre-release for the version you intend to release and creates a new `bump-versions-<next_version>` branch to bump the version after the release so future changes apply to a new in-progress version instead of to the already released version. This workflow has the following inputs:

- `version`: Enter the version you intend to publish (e.g. 0.2.0). This is simply for verification to make sure you release the code that you intend to release. It is compared to the version in the code, and the workflow will fail if they do not match.
- `newVersionAfterPublishing`: Enter the version you want to bump to after releasing (e.g. 0.3.0-alpha.0). Future changes will apply to this new version instead of to the version that was already released. Leave blank if you don't want to bump.
- `bumpRef`: Enter the Git ref you want to create the bump versions branch from, e.g. `main`. Leave blank if you want to use the branch selected for the workflow run. For example, if you release from a stable branch named `release-prep`, you may want to bump the version on `main` so future development work happens on the new version, then you can rebase `release-prep` onto `main` when you are ready to start preparing the next stable release.
Expand Down
8 changes: 8 additions & 0 deletions __mocks__/fileMock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* @file Jest mock for static asset imports (images, fonts, etc.). Importing e.g. `logo.png` in tests will
* receive this string instead of running file loaders. Mirrors webpack's asset/inline and
* asset/resource handling in webpack.config.base.
*
* @see https://jestjs.io/docs/webpack#handling-static-assets
*/
module.exports = 'test-file-stub';
9 changes: 9 additions & 0 deletions __mocks__/interlinearXmlContent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* @file Jest mock for webpack ?raw XML import. Exports the contents of test-data/Interlinear_en_MAT.xml
* so interlinearizer.web-view.tsx can parse it in unit tests without webpack.
*/
import fs from 'fs';
import path from 'path';

const xmlPath = path.join(__dirname, '..', 'test-data', 'Interlinear_en_MAT.xml');
module.exports = fs.readFileSync(xmlPath, 'utf-8');
41 changes: 41 additions & 0 deletions __mocks__/papi-backend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* @file Jest mock for @papi/backend. Provides papi and logger so main.ts can be unit-tested without
* loading the real Platform API.
*/

const mockRegisterWebViewProvider = jest.fn().mockResolvedValue({ dispose: jest.fn() });
const mockOpenWebView = jest.fn().mockResolvedValue(undefined);
const mockLogger = {
debug: jest.fn(),
error: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
};

const papi = {
webViewProviders: {
registerWebViewProvider: mockRegisterWebViewProvider,
},
webViews: {
openWebView: mockOpenWebView,
},
};

const defaultExport = {
...papi,
__mockRegisterWebViewProvider: mockRegisterWebViewProvider,
__mockOpenWebView: mockOpenWebView,
__mockLogger: mockLogger,
};

module.exports = {
__esModule: true,
default: defaultExport,
logger: mockLogger,
__mockRegisterWebViewProvider: mockRegisterWebViewProvider,
__mockOpenWebView: mockOpenWebView,
__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). */
export {};
6 changes: 6 additions & 0 deletions __mocks__/papi-core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* @file Jest mock for @papi/core. main.ts imports only types from here (ExecutionActivationContext,
* IWebViewProvider, SavedWebViewDefinition, WebViewDefinition); types are erased at runtime so this
* mock only needs to exist for module resolution.
*/
module.exports = {};
63 changes: 63 additions & 0 deletions __mocks__/papi-frontend-react.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* @file Jest mock for @papi/frontend/react. Provides stub implementations of various 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].
*/
const createUseDataLikeHook = () =>
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);
const useProjectSetting = jest
.fn()
.mockImplementation((_projectInterface: string, _projectIdOrPdp: unknown, _key: string, defaultState: unknown) => [
defaultState,
jest.fn(),
]);
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([]);

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,
};
60 changes: 60 additions & 0 deletions __mocks__/papi-frontend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* @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.
*/

const mockLogger = {
debug: jest.fn(),
error: jest.fn(),
info: jest.fn(),
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). */
export {};
Loading
Loading