Skip to content

Commit e0d227c

Browse files
Add Interlinearization model, PT9 refactor, and viewing-mode support
- Introduce `Interlinearization` data model. - Move internal-only PT9 types to dedicated file and change case of props. - Removed `ScrTextName` prop as it's been deprecated in PT9 since 2020. - Enhance interlinearizer WebView to support switching between viewing modes: InterlinearData and Interlinearization. - Update Jest configuration to include path aliases for types and parsers. - Modify README to clarify the structure of the `src/types/` and `src/parsers/` directories. - Rename `interlinearXmlParser` and related tests to `paratext9parser`. - Add new words to cspell configuration for improved spell checking.
1 parent 1e90d37 commit e0d227c

14 files changed

Lines changed: 1617 additions & 244 deletions

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ The general file structure for an extension is as follows:
9595
- `src/` contains the source code for the extension
9696
- `src/main.ts` is the main entry file for the extension (registers commands and wires interlinear XML)
9797
- `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
98-
- `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`
98+
- `src/types/` also holds shared enums and type modules (e.g. `interlinearizer-enums.ts`). Use the path alias `types/interlinearizer-enums` in imports instead of relative paths (see `tsconfig.json` paths).
99+
- `src/parsers/` contains all parsers and converters used when importing external data models sorted by source (e.g. Paratext 9 XML Files). Use the path alias `parsers/...` in imports instead of relative paths (see `tsconfig.json` paths).
99100
- `*.web-view.tsx` files will be treated as React WebViews
100101
- `*.web-view.scss` files provide styles for WebViews
101102
- `*.web-view.html` files are a conventional way to provide HTML WebViews (no special functionality)

cspell.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,25 @@
1616
"appdata",
1717
"asyncs",
1818
"autodocs",
19+
"BCVWP",
20+
"behaviour",
1921
"dockbox",
22+
"Eflomal",
2023
"electronmon",
2124
"endregion",
2225
"finalizer",
2326
"Fragmenter",
2427
"guids",
2528
"hopkinson",
2629
"iframes",
30+
"interlineardata",
2731
"interlinearization",
32+
"interlinearizations",
2833
"interlinearizer",
34+
"jsmith",
2935
"localstorage",
3036
"maximizable",
37+
"Morphosyntactic",
3138
"networkable",
3239
"Newtonsoft",
3340
"nodebuffer",
@@ -40,22 +47,26 @@
4047
"pdps",
4148
"plusplus",
4249
"proxied",
50+
"punc",
4351
"reinitializing",
4452
"reserialized",
4553
"sillsdev",
4654
"steenwyk",
4755
"stringifiable",
4856
"Stylesheet",
4957
"typedefs",
58+
"unanalyzed",
5059
"unregistering",
5160
"unregisters",
61+
"unreviewed",
5262
"unsub",
5363
"unsubs",
5464
"unsubscriber",
5565
"unsubscribers",
5666
"usfm",
5767
"verseref",
58-
"versification"
68+
"versification",
69+
"wordform"
5970
],
6071
"ignoreWords": [],
6172
"import": []

jest.config.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const config: Config = {
2727
'src/parsers/**/*.ts',
2828
'src/main.ts',
2929
'src/**/*.web-view.tsx',
30-
'!src/parsers/**/*.d.ts',
30+
'!src/parsers/**/*-types.ts',
3131
'!src/**/__tests__/**',
3232
'!src/**/*.test.{ts,tsx}',
3333
'!src/**/*.spec.{ts,tsx}',
@@ -70,11 +70,12 @@ const config: Config = {
7070
*/
7171
moduleNameMapper: {
7272
/**
73-
* Resolve src-rooted path aliases so tests can use e.g. "@main" or "parsers/..." instead of
74-
* relative paths. Must match tsconfig.json "paths" and webpack resolve.alias.
73+
* Resolve src-rooted path aliases so tests can use e.g. "@main", "parsers/...", or "types/..."
74+
* instead of relative paths. Must match tsconfig.json "paths" and webpack resolve.alias.
7575
*/
7676
'^@main$': '<rootDir>/src/main',
7777
'^parsers/(.*)$': '<rootDir>/src/parsers/$1',
78+
'^types/(.*)$': '<rootDir>/src/types/$1',
7879
'\\.(sa|sc|c)ss$': '<rootDir>/__mocks__/styleMock.ts',
7980
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
8081
'<rootDir>/__mocks__/fileMock.ts',

src/__tests__/interlinearizer.web-view.test.tsx

Lines changed: 88 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,38 @@
44

55
import type { WebViewProps } from '@papi/core';
66
import type { SerializedVerseRef } from '@sillsdev/scripture';
7-
import { render, screen } from '@testing-library/react';
8-
import { InterlinearXmlParser } from 'parsers/interlinearXmlParser';
9-
10-
/** Mock parser to allow overriding constructor behavior per test. */
11-
jest.mock('parsers/interlinearXmlParser', () => {
12-
const actual = jest.requireActual<typeof import('parsers/interlinearXmlParser')>(
13-
'parsers/interlinearXmlParser',
14-
);
15-
return {
16-
InterlinearXmlParser: jest.fn().mockImplementation(() => new actual.InterlinearXmlParser()),
17-
};
18-
});
7+
import { fireEvent, render, screen } from '@testing-library/react';
8+
import type { InterlinearData } from 'paratext-9-types';
9+
10+
/** Stub InterlinearData returned by the mocked parser. Matches shape the WebView displays. */
11+
const stubInterlinearData: InterlinearData = {
12+
glossLanguage: 'en',
13+
bookId: 'MAT',
14+
verses: {},
15+
};
16+
17+
/** Stub Interlinearization returned by the mocked converter. Matches shape the WebView displays. */
18+
const stubInterlinearization = {
19+
id: 'mock-interlinear-id',
20+
sourceWritingSystem: '',
21+
analysisLanguages: ['en'],
22+
books: [{ id: 'mock-book-id', bookRef: 'MAT', textVersion: '', segments: [] }],
23+
};
24+
25+
const mockParse = jest.fn().mockReturnValue(stubInterlinearData);
26+
const mockConvert = jest.fn().mockReturnValue(stubInterlinearization);
27+
28+
/** Mock parser: no real XML parsing; returns stub data. Parser/converter are tested elsewhere. */
29+
jest.mock('parsers/paratext-9/paratext9Parser', () => ({
30+
Paratext9Parser: jest.fn().mockImplementation(() => ({
31+
parse: mockParse,
32+
})),
33+
}));
34+
35+
/** Mock converter: no real conversion; returns stub Interlinearization. */
36+
jest.mock('parsers/paratext-9/paratext9Converter', () => ({
37+
convertParatext9ToInterlinearization: mockConvert,
38+
}));
1939

2040
/**
2141
* Load the WebView module; it assigns the component to globalThis.webViewComponent. This pattern is
@@ -66,56 +86,90 @@ describe('InterlinearizerWebView', () => {
6686
expect(screen.getByText(/test-data\/Interlinear_en_MAT\.xml/i)).toBeInTheDocument();
6787
});
6888

69-
it('parses the bundled test XML and displays parsed JSON', () => {
89+
it('renders the JSON view mode switch (InterlinearData / Interlinearization)', () => {
7090
render(<InterlinearizerWebView {...testWebViewProps} />);
7191

72-
expect(screen.getByText(/parsed interlinear data \(json\)/i)).toBeInTheDocument();
73-
expect(screen.getByText(/"GlossLanguage"/)).toBeInTheDocument();
74-
expect(screen.getByText(/"BookId"/)).toBeInTheDocument();
92+
const group = screen.getByRole('group', { name: /json view mode/i });
93+
expect(group).toBeInTheDocument();
94+
expect(screen.getByRole('button', { name: /^interlineardata$/i })).toBeInTheDocument();
95+
expect(screen.getByRole('button', { name: /^interlinearization$/i })).toBeInTheDocument();
96+
expect(screen.getByText(/view json as:/i)).toBeInTheDocument();
7597
});
7698

77-
it('displays parsed structure with expected verse data', () => {
99+
it('displays InterlinearData JSON by default when parser returns data', () => {
100+
render(<InterlinearizerWebView {...testWebViewProps} />);
101+
102+
expect(screen.getByText(/^InterlinearData \(JSON\):$/)).toBeInTheDocument();
103+
expect(screen.getByText(/glossLanguage/i)).toBeInTheDocument();
104+
expect(screen.getByText(/bookId/i)).toBeInTheDocument();
105+
});
106+
107+
it('displays parsed structure including glossLanguage and bookId values', () => {
78108
render(<InterlinearizerWebView {...testWebViewProps} />);
79109

80110
expect(screen.getByText(/"en"/)).toBeInTheDocument();
81111
expect(screen.getByText(/"MAT"/)).toBeInTheDocument();
82112
});
83113

84-
it('does not show parse error when XML is valid', () => {
114+
it('does not show parse error when parser succeeds', () => {
85115
render(<InterlinearizerWebView {...testWebViewProps} />);
86116

87117
expect(screen.queryByText(/^parse error$/i)).not.toBeInTheDocument();
88118
});
89119

90120
it('displays parse error when parser throws an Error (uses err.message)', () => {
91-
const actual = jest.requireActual<typeof import('../parsers/interlinearXmlParser')>(
92-
'../parsers/interlinearXmlParser',
93-
);
94-
const realInstance = new actual.InterlinearXmlParser();
95-
const throwingParse = (): never => {
121+
mockParse.mockImplementationOnce(() => {
96122
throw new Error('Invalid XML structure');
97-
};
98-
Object.defineProperty(realInstance, 'parse', { value: throwingParse, writable: true });
99-
jest.mocked(InterlinearXmlParser).mockImplementationOnce(() => realInstance);
123+
});
100124

101125
render(<InterlinearizerWebView {...testWebViewProps} />);
102126

103127
expect(screen.getByRole('heading', { name: /^parse error$/i })).toBeInTheDocument();
104128
expect(screen.getByText(/invalid xml structure/i)).toBeInTheDocument();
105129
});
106130

131+
it('switching to Interlinearization shows converted model JSON', () => {
132+
render(<InterlinearizerWebView {...testWebViewProps} />);
133+
134+
fireEvent.click(screen.getByRole('button', { name: /^interlinearization$/i }));
135+
136+
expect(screen.getByText(/^Interlinearization \(JSON\):$/)).toBeInTheDocument();
137+
expect(screen.getByText(/analysisLanguages/i)).toBeInTheDocument();
138+
expect(screen.getByText(/sourceWritingSystem/i)).toBeInTheDocument();
139+
expect(screen.getByText(/segments/i)).toBeInTheDocument();
140+
});
141+
142+
it('switching back to InterlinearData shows PT9 structure JSON', () => {
143+
render(<InterlinearizerWebView {...testWebViewProps} />);
144+
145+
fireEvent.click(screen.getByRole('button', { name: /^interlinearization$/i }));
146+
expect(screen.getByText(/^Interlinearization \(JSON\):$/)).toBeInTheDocument();
147+
148+
fireEvent.click(screen.getByRole('button', { name: /^interlineardata$/i }));
149+
150+
expect(screen.getByText(/^InterlinearData \(JSON\):$/)).toBeInTheDocument();
151+
expect(screen.getByText(/glossLanguage/i)).toBeInTheDocument();
152+
expect(screen.getByText(/bookId/i)).toBeInTheDocument();
153+
});
154+
155+
it('renders empty JSON pre when jsonToShow is undefined (converter returns undefined)', () => {
156+
mockConvert.mockReturnValueOnce(undefined);
157+
158+
const { container } = render(<InterlinearizerWebView {...testWebViewProps} />);
159+
fireEvent.click(screen.getByRole('button', { name: /^interlinearization$/i }));
160+
161+
const jsonPre = container.querySelector('pre');
162+
expect(jsonPre).toBeInTheDocument();
163+
expect(jsonPre).toBeEmptyDOMElement();
164+
expect(jsonPre).not.toHaveTextContent('undefined');
165+
});
166+
107167
it('displays parse error when parser throws non-Error (uses String(err))', () => {
108-
const actual = jest.requireActual<typeof import('../parsers/interlinearXmlParser')>(
109-
'../parsers/interlinearXmlParser',
110-
);
111-
const realInstance = new actual.InterlinearXmlParser();
112-
const throwingParse = (): never => {
168+
mockParse.mockImplementationOnce(() => {
113169
// Intentionally throw a non-Error to test the String(err) branch in the catch block.
114170
// eslint-disable-next-line no-throw-literal -- testing non-Error handling
115171
throw 'plain string error';
116-
};
117-
Object.defineProperty(realInstance, 'parse', { value: throwingParse, writable: true });
118-
jest.mocked(InterlinearXmlParser).mockImplementationOnce(() => realInstance);
172+
});
119173

120174
render(<InterlinearizerWebView {...testWebViewProps} />);
121175

0 commit comments

Comments
 (0)