Skip to content

Commit 26627c8

Browse files
committed
feat: Add Phase 1 Grid3 utilities (GUID, Settings XML, FileMap XML)
- Add generateGrid3Guid() for creating unique Grid3 element identifiers - Add createSettingsXml() for building Grid3 settings configuration - Add createFileMapXml() for creating Grid3 FileMap.xml content - Export new utilities from src/processors/index.ts - Add comprehensive unit tests (16 new tests, all passing) - All tests pass: 406 passed, 1 skipped - No regressions in existing functionality
1 parent e162a29 commit 26627c8

4 files changed

Lines changed: 510 additions & 1 deletion

File tree

src/processors/gridset/helpers.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import AdmZip from 'adm-zip';
2+
import { XMLBuilder } from 'fast-xml-parser';
23
import { AACTree, AACPage, AACButton } from '../../core/treeStructure';
34

45
function normalizeZipPath(p: string): string {
@@ -39,3 +40,88 @@ export function openImage(gridsetBuffer: Buffer, entryPath: string): Buffer | nu
3940
if (!entry) return null;
4041
return entry.getData();
4142
}
43+
44+
/**
45+
* Generate a random GUID for Grid3 elements
46+
* Grid3 uses GUIDs for grid identification
47+
* @returns A UUID v4-like string in the format xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
48+
*/
49+
export function generateGrid3Guid(): string {
50+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
51+
const r = (Math.random() * 16) | 0;
52+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
53+
return v.toString(16);
54+
});
55+
}
56+
57+
/**
58+
* Create Grid3 settings XML with start grid and common settings
59+
* @param startGrid - Name of the grid to start on
60+
* @param options - Optional settings (scan, hover, language, etc.)
61+
* @returns XML string for Settings.xml
62+
*/
63+
export function createSettingsXml(
64+
startGrid: string,
65+
options?: {
66+
scanEnabled?: boolean;
67+
scanTimeoutMs?: number;
68+
hoverEnabled?: boolean;
69+
hoverTimeoutMs?: number;
70+
mouseclickEnabled?: boolean;
71+
language?: string;
72+
}
73+
): string {
74+
const builder = new XMLBuilder({
75+
ignoreAttributes: false,
76+
format: true,
77+
indentBy: ' ',
78+
});
79+
80+
const settingsData = {
81+
GridSetSettings: {
82+
'@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
83+
StartGrid: startGrid,
84+
ScanEnabled: options?.scanEnabled?.toString() ?? 'false',
85+
ScanTimeoutMs: options?.scanTimeoutMs?.toString() ?? '2000',
86+
HoverEnabled: options?.hoverEnabled?.toString() ?? 'false',
87+
HoverTimeoutMs: options?.hoverTimeoutMs?.toString() ?? '1000',
88+
MouseclickEnabled: options?.mouseclickEnabled?.toString() ?? 'true',
89+
Language: options?.language ?? 'en-US',
90+
},
91+
};
92+
93+
return builder.build(settingsData);
94+
}
95+
96+
/**
97+
* Create Grid3 FileMap.xml content
98+
* @param grids - Array of grid configurations with name and path
99+
* @returns XML string for FileMap.xml
100+
*/
101+
export function createFileMapXml(
102+
grids: Array<{ name: string; path: string; dynamicFiles?: string[] }>
103+
): string {
104+
const builder = new XMLBuilder({
105+
ignoreAttributes: false,
106+
format: true,
107+
indentBy: ' ',
108+
});
109+
110+
const entries = grids.map((grid) => ({
111+
'@_StaticFile': grid.path,
112+
...(grid.dynamicFiles && grid.dynamicFiles.length > 0
113+
? { DynamicFiles: { File: grid.dynamicFiles } }
114+
: {}),
115+
}));
116+
117+
const fileMapData = {
118+
FileMap: {
119+
'@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
120+
Entries: {
121+
Entry: entries,
122+
},
123+
},
124+
};
125+
126+
return builder.build(fileMapData);
127+
}

src/processors/index.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,21 @@ export { TouchChatProcessor } from './touchchatProcessor';
99
export { AstericsGridProcessor } from './astericsGridProcessor';
1010

1111
// Gridset (Grid 3) helpers
12-
export { getPageTokenImageMap, getAllowedImageEntries, openImage } from './gridset/helpers';
12+
export {
13+
getPageTokenImageMap,
14+
getAllowedImageEntries,
15+
openImage,
16+
generateGrid3Guid,
17+
createSettingsXml,
18+
createFileMapXml,
19+
} from './gridset/helpers';
1320
export {
1421
getPageTokenImageMap as getGridsetPageTokenImageMap,
1522
getAllowedImageEntries as getGridsetAllowedImageEntries,
1623
openImage as openGridsetImage,
24+
generateGrid3Guid as generateGridsetGuid,
25+
createSettingsXml as createGridsetSettingsXml,
26+
createFileMapXml as createGridsetFileMapXml,
1727
} from './gridset/helpers';
1828
export { resolveGrid3CellImage } from './gridset/resolver';
1929

test/gridsetHelpers.test.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import {
44
getAllowedImageEntries,
55
getPageTokenImageMap,
66
openImage,
7+
generateGrid3Guid,
8+
createSettingsXml,
9+
createFileMapXml,
710
} from '../src/processors/gridset/helpers';
811

912
describe('Gridset helper APIs', () => {
@@ -75,3 +78,136 @@ describe('Gridset helper APIs', () => {
7578
expect(missing).toBeNull();
7679
});
7780
});
81+
82+
describe('Grid3 GUID Generation', () => {
83+
it('generateGrid3Guid generates a valid GUID format', () => {
84+
const guid = generateGrid3Guid();
85+
// Check format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
86+
const guidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
87+
expect(guid).toMatch(guidRegex);
88+
});
89+
90+
it('generateGrid3Guid generates unique GUIDs', () => {
91+
const guid1 = generateGrid3Guid();
92+
const guid2 = generateGrid3Guid();
93+
const guid3 = generateGrid3Guid();
94+
expect(guid1).not.toBe(guid2);
95+
expect(guid2).not.toBe(guid3);
96+
expect(guid1).not.toBe(guid3);
97+
});
98+
99+
it('generateGrid3Guid generates GUIDs with correct version and variant', () => {
100+
// Generate multiple GUIDs and check they all have version 4 and variant 1
101+
for (let i = 0; i < 10; i++) {
102+
const guid = generateGrid3Guid();
103+
const parts = guid.split('-');
104+
// Version 4 is in the first character of the 3rd group
105+
expect(parts[2][0]).toBe('4');
106+
// Variant 1 is in the first character of the 4th group (should be 8, 9, a, or b)
107+
expect(['8', '9', 'a', 'b']).toContain(parts[3][0].toLowerCase());
108+
}
109+
});
110+
});
111+
112+
describe('Grid3 Settings XML Builder', () => {
113+
it('createSettingsXml creates valid XML with default options', () => {
114+
const xml = createSettingsXml('Home');
115+
expect(xml).toContain('<GridSetSettings');
116+
expect(xml).toContain('<StartGrid>Home</StartGrid>');
117+
expect(xml).toContain('<ScanEnabled>false</ScanEnabled>');
118+
expect(xml).toContain('<HoverEnabled>false</HoverEnabled>');
119+
expect(xml).toContain('<MouseclickEnabled>true</MouseclickEnabled>');
120+
expect(xml).toContain('<Language>en-US</Language>');
121+
});
122+
123+
it('createSettingsXml respects custom options', () => {
124+
const xml = createSettingsXml('MainMenu', {
125+
scanEnabled: true,
126+
scanTimeoutMs: 3000,
127+
hoverEnabled: true,
128+
hoverTimeoutMs: 1500,
129+
mouseclickEnabled: false,
130+
language: 'fr-FR',
131+
});
132+
expect(xml).toContain('<StartGrid>MainMenu</StartGrid>');
133+
expect(xml).toContain('<ScanEnabled>true</ScanEnabled>');
134+
expect(xml).toContain('<ScanTimeoutMs>3000</ScanTimeoutMs>');
135+
expect(xml).toContain('<HoverEnabled>true</HoverEnabled>');
136+
expect(xml).toContain('<HoverTimeoutMs>1500</HoverTimeoutMs>');
137+
expect(xml).toContain('<MouseclickEnabled>false</MouseclickEnabled>');
138+
expect(xml).toContain('<Language>fr-FR</Language>');
139+
});
140+
141+
it('createSettingsXml includes XML namespace', () => {
142+
const xml = createSettingsXml('Home');
143+
expect(xml).toContain('xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"');
144+
});
145+
146+
it('createSettingsXml handles partial options', () => {
147+
const xml = createSettingsXml('Home', {
148+
scanEnabled: true,
149+
language: 'de-DE',
150+
});
151+
expect(xml).toContain('<ScanEnabled>true</ScanEnabled>');
152+
expect(xml).toContain('<Language>de-DE</Language>');
153+
// Should still have defaults for unspecified options
154+
expect(xml).toContain('<HoverEnabled>false</HoverEnabled>');
155+
expect(xml).toContain('<MouseclickEnabled>true</MouseclickEnabled>');
156+
});
157+
});
158+
159+
describe('Grid3 FileMap XML Builder', () => {
160+
it('createFileMapXml creates valid XML with single grid', () => {
161+
const xml = createFileMapXml([{ name: 'Home', path: 'Grids\\Home\\grid.xml' }]);
162+
expect(xml).toContain('<FileMap');
163+
expect(xml).toContain('<Entries>');
164+
expect(xml).toContain('<Entry');
165+
expect(xml).toContain('StaticFile="Grids\\Home\\grid.xml"');
166+
});
167+
168+
it('createFileMapXml creates valid XML with multiple grids', () => {
169+
const xml = createFileMapXml([
170+
{ name: 'Home', path: 'Grids\\Home\\grid.xml' },
171+
{ name: 'Menu', path: 'Grids\\Menu\\grid.xml' },
172+
{ name: 'Settings', path: 'Grids\\Settings\\grid.xml' },
173+
]);
174+
expect(xml).toContain('StaticFile="Grids\\Home\\grid.xml"');
175+
expect(xml).toContain('StaticFile="Grids\\Menu\\grid.xml"');
176+
expect(xml).toContain('StaticFile="Grids\\Settings\\grid.xml"');
177+
});
178+
179+
it('createFileMapXml includes dynamic files when provided', () => {
180+
const xml = createFileMapXml([
181+
{
182+
name: 'Home',
183+
path: 'Grids\\Home\\grid.xml',
184+
dynamicFiles: ['dynamic1.xml', 'dynamic2.xml'],
185+
},
186+
]);
187+
expect(xml).toContain('<DynamicFiles>');
188+
expect(xml).toContain('<File>dynamic1.xml</File>');
189+
expect(xml).toContain('<File>dynamic2.xml</File>');
190+
});
191+
192+
it('createFileMapXml omits DynamicFiles when empty', () => {
193+
const xml = createFileMapXml([
194+
{ name: 'Home', path: 'Grids\\Home\\grid.xml', dynamicFiles: [] },
195+
]);
196+
expect(xml).not.toContain('<DynamicFiles>');
197+
});
198+
199+
it('createFileMapXml includes XML namespace', () => {
200+
const xml = createFileMapXml([{ name: 'Home', path: 'Grids\\Home\\grid.xml' }]);
201+
expect(xml).toContain('xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"');
202+
});
203+
204+
it('createFileMapXml handles mixed grids with and without dynamic files', () => {
205+
const xml = createFileMapXml([
206+
{ name: 'Home', path: 'Grids\\Home\\grid.xml' },
207+
{ name: 'Menu', path: 'Grids\\Menu\\grid.xml', dynamicFiles: ['menu_dynamic.xml'] },
208+
]);
209+
expect(xml).toContain('StaticFile="Grids\\Home\\grid.xml"');
210+
expect(xml).toContain('StaticFile="Grids\\Menu\\grid.xml"');
211+
expect(xml).toContain('<File>menu_dynamic.xml</File>');
212+
});
213+
});

0 commit comments

Comments
 (0)