@willwade/aac-processors 0.0.18 → 0.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/baseProcessor.d.ts +1 -1
- package/dist/core/baseProcessor.js +3 -3
- package/dist/core/treeStructure.d.ts +7 -2
- package/dist/core/treeStructure.js +17 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/processors/applePanelsProcessor.js +8 -6
- package/dist/processors/astericsGridProcessor.js +38 -2
- package/dist/processors/dotProcessor.js +5 -1
- package/dist/processors/excelProcessor.js +8 -3
- package/dist/processors/gridsetProcessor.js +77 -3
- package/dist/processors/obfProcessor.js +47 -6
- package/dist/processors/obfsetProcessor.js +1 -0
- package/dist/processors/opmlProcessor.js +1 -0
- package/dist/processors/snapProcessor.d.ts +18 -0
- package/dist/processors/snapProcessor.js +88 -2
- package/dist/processors/touchchatProcessor.js +2 -0
- package/dist/types/aac.d.ts +66 -2
- package/dist/utilities/analytics/metrics/core.js +4 -1
- package/dist/validation/gridsetValidator.d.ts +12 -0
- package/dist/validation/gridsetValidator.js +92 -5
- package/package.json +1 -1
|
@@ -130,7 +130,7 @@ class BaseProcessor {
|
|
|
130
130
|
const key = page.name.trim().toLowerCase();
|
|
131
131
|
const vocabLocation = {
|
|
132
132
|
table: 'pages',
|
|
133
|
-
id:
|
|
133
|
+
id: page.id,
|
|
134
134
|
column: 'NAME',
|
|
135
135
|
casing: (0, stringCasing_1.detectCasing)(page.name),
|
|
136
136
|
};
|
|
@@ -142,7 +142,7 @@ class BaseProcessor {
|
|
|
142
142
|
const key = button.label.trim().toLowerCase();
|
|
143
143
|
const vocabLocation = {
|
|
144
144
|
table: 'buttons',
|
|
145
|
-
id:
|
|
145
|
+
id: button.id,
|
|
146
146
|
column: 'LABEL',
|
|
147
147
|
casing: (0, stringCasing_1.detectCasing)(button.label),
|
|
148
148
|
};
|
|
@@ -156,7 +156,7 @@ class BaseProcessor {
|
|
|
156
156
|
const key = button.message.trim().toLowerCase();
|
|
157
157
|
const vocabLocation = {
|
|
158
158
|
table: 'buttons',
|
|
159
|
-
id:
|
|
159
|
+
id: button.id,
|
|
160
160
|
column: 'MESSAGE',
|
|
161
161
|
casing: (0, stringCasing_1.detectCasing)(button.message),
|
|
162
162
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { AACButton as IAACButton, AACPage as IAACPage, AACTree as IAACTree, AACStyle } from '../types/aac';
|
|
1
|
+
import { AACButton as IAACButton, AACPage as IAACPage, AACTree as IAACTree, AACTreeMetadata, SnapMetadata, GridSetMetadata, AstericsGridMetadata, TouchChatMetadata, AACStyle } from '../types/aac';
|
|
2
|
+
export { AACTreeMetadata, SnapMetadata, GridSetMetadata, AstericsGridMetadata, TouchChatMetadata };
|
|
2
3
|
export declare enum AACSemanticCategory {
|
|
3
4
|
COMMUNICATION = "communication",// Speech, text output
|
|
4
5
|
NAVIGATION = "navigation",// Page/grid navigation
|
|
@@ -234,9 +235,13 @@ export declare class AACTree implements IAACTree {
|
|
|
234
235
|
pages: {
|
|
235
236
|
[key: string]: AACPage;
|
|
236
237
|
};
|
|
237
|
-
|
|
238
|
+
metadata: AACTreeMetadata;
|
|
238
239
|
get rootId(): string | null;
|
|
239
240
|
set rootId(id: string | null);
|
|
241
|
+
get toolbarId(): string | null;
|
|
242
|
+
set toolbarId(id: string | null);
|
|
243
|
+
get dashboardId(): string | null;
|
|
244
|
+
set dashboardId(id: string | null);
|
|
240
245
|
constructor();
|
|
241
246
|
addPage(page: AACPage): void;
|
|
242
247
|
getPage(id: string): AACPage | undefined;
|
|
@@ -202,19 +202,31 @@ class AACPage {
|
|
|
202
202
|
exports.AACPage = AACPage;
|
|
203
203
|
class AACTree {
|
|
204
204
|
get rootId() {
|
|
205
|
-
return this.
|
|
205
|
+
return this.metadata.defaultHomePageId || null;
|
|
206
206
|
}
|
|
207
207
|
set rootId(id) {
|
|
208
|
-
this.
|
|
208
|
+
this.metadata.defaultHomePageId = id || undefined;
|
|
209
|
+
}
|
|
210
|
+
get toolbarId() {
|
|
211
|
+
return this.metadata.toolbarId || null;
|
|
212
|
+
}
|
|
213
|
+
set toolbarId(id) {
|
|
214
|
+
this.metadata.toolbarId = id || undefined;
|
|
215
|
+
}
|
|
216
|
+
get dashboardId() {
|
|
217
|
+
return this.metadata.dashboardId || null;
|
|
218
|
+
}
|
|
219
|
+
set dashboardId(id) {
|
|
220
|
+
this.metadata.dashboardId = id || undefined;
|
|
209
221
|
}
|
|
210
222
|
constructor() {
|
|
211
223
|
this.pages = {};
|
|
212
|
-
this.
|
|
224
|
+
this.metadata = {};
|
|
213
225
|
}
|
|
214
226
|
addPage(page) {
|
|
215
227
|
this.pages[page.id] = page;
|
|
216
|
-
if (!this.
|
|
217
|
-
this.
|
|
228
|
+
if (!this.rootId)
|
|
229
|
+
this.rootId = page.id;
|
|
218
230
|
}
|
|
219
231
|
getPage(id) {
|
|
220
232
|
return this.pages[id];
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export * from './core/baseProcessor';
|
|
|
10
10
|
export * from './core/stringCasing';
|
|
11
11
|
export * from './processors';
|
|
12
12
|
export * as Analytics from './utilities/analytics';
|
|
13
|
+
export * from './utilities/analytics';
|
|
13
14
|
export * as Validation from './validation';
|
|
14
15
|
export * as Gridset from './gridset';
|
|
15
16
|
export * as Snap from './snap';
|
package/dist/index.js
CHANGED
|
@@ -52,6 +52,8 @@ __exportStar(require("./processors"), exports);
|
|
|
52
52
|
// ===================================================================
|
|
53
53
|
// Analytics namespace
|
|
54
54
|
exports.Analytics = __importStar(require("./utilities/analytics"));
|
|
55
|
+
// Also export analytics classes directly for convenience
|
|
56
|
+
__exportStar(require("./utilities/analytics"), exports);
|
|
55
57
|
// Validation namespace
|
|
56
58
|
exports.Validation = __importStar(require("./validation"));
|
|
57
59
|
// Processor namespaces (platform-specific utilities)
|
|
@@ -139,6 +139,7 @@ class ApplePanelsProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
139
139
|
}
|
|
140
140
|
const data = { panels: panelsData };
|
|
141
141
|
const tree = new treeStructure_1.AACTree();
|
|
142
|
+
tree.metadata.format = 'applepanels';
|
|
142
143
|
data.panels.forEach((panel) => {
|
|
143
144
|
const page = new treeStructure_1.AACPage({
|
|
144
145
|
id: panel.id,
|
|
@@ -309,16 +310,17 @@ class ApplePanelsProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
309
310
|
fs_1.default.mkdirSync(resourcesPath, { recursive: true });
|
|
310
311
|
// Create Info.plist (bundle mode only)
|
|
311
312
|
const infoPlist = {
|
|
312
|
-
ASCConfigurationDisplayName: 'AAC Processors Export',
|
|
313
|
+
ASCConfigurationDisplayName: tree.metadata?.name || 'AAC Processors Export',
|
|
313
314
|
ASCConfigurationIdentifier: `com.aacprocessors.${Date.now()}`,
|
|
314
315
|
ASCConfigurationProductSupportType: 'VirtualKeyboard',
|
|
315
|
-
ASCConfigurationVersion: '7.1',
|
|
316
|
-
CFBundleDevelopmentRegion: 'en',
|
|
316
|
+
ASCConfigurationVersion: tree.metadata?.version || '7.1',
|
|
317
|
+
CFBundleDevelopmentRegion: tree.metadata?.locale || 'en',
|
|
317
318
|
CFBundleIdentifier: 'com.aacprocessors.panel.export',
|
|
318
|
-
CFBundleName: 'AAC Processors Panels',
|
|
319
|
-
CFBundleShortVersionString: '1.0',
|
|
319
|
+
CFBundleName: tree.metadata?.name || 'AAC Processors Panels',
|
|
320
|
+
CFBundleShortVersionString: tree.metadata?.version || '1.0',
|
|
320
321
|
CFBundleVersion: '1',
|
|
321
|
-
NSHumanReadableCopyright:
|
|
322
|
+
NSHumanReadableCopyright: tree.metadata?.copyright ||
|
|
323
|
+
`Generated by AAC Processors${tree.metadata?.author ? ` - Author: ${tree.metadata.author}` : ''}`,
|
|
322
324
|
};
|
|
323
325
|
const infoPlistContent = plist_1.default.build(infoPlist);
|
|
324
326
|
fs_1.default.writeFileSync(path_1.default.join(contentsPath, 'Info.plist'), infoPlistContent);
|
|
@@ -740,6 +740,41 @@ class AstericsGridProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
740
740
|
// Set the page's grid layout
|
|
741
741
|
page.grid = gridLayout;
|
|
742
742
|
});
|
|
743
|
+
// Set metadata for Asterics Grid files
|
|
744
|
+
const astericsMetadata = {
|
|
745
|
+
format: 'asterics',
|
|
746
|
+
hasGlobalGrid: false, // Can be extended in the future
|
|
747
|
+
};
|
|
748
|
+
if (grdFile.grids && grdFile.grids.length > 0) {
|
|
749
|
+
astericsMetadata.name = this.getLocalizedLabel(grdFile.grids[0].label);
|
|
750
|
+
// Extract all unique languages from all grids and elements
|
|
751
|
+
const languages = new Set();
|
|
752
|
+
grdFile.grids.forEach((grid) => {
|
|
753
|
+
if (grid.label) {
|
|
754
|
+
Object.keys(grid.label).forEach((lang) => languages.add(lang));
|
|
755
|
+
}
|
|
756
|
+
grid.gridElements?.forEach((element) => {
|
|
757
|
+
if (element.label) {
|
|
758
|
+
Object.keys(element.label).forEach((lang) => languages.add(lang));
|
|
759
|
+
}
|
|
760
|
+
// Also check word forms for languages
|
|
761
|
+
element.wordForms?.forEach((wf) => {
|
|
762
|
+
if (wf.lang)
|
|
763
|
+
languages.add(wf.lang);
|
|
764
|
+
});
|
|
765
|
+
});
|
|
766
|
+
});
|
|
767
|
+
if (languages.size > 0) {
|
|
768
|
+
astericsMetadata.languages = Array.from(languages).sort();
|
|
769
|
+
// Set primary locale to English if available, otherwise the first language found
|
|
770
|
+
astericsMetadata.locale = languages.has('en')
|
|
771
|
+
? 'en'
|
|
772
|
+
: languages.has('de')
|
|
773
|
+
? 'de'
|
|
774
|
+
: astericsMetadata.languages[0];
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
tree.metadata = astericsMetadata;
|
|
743
778
|
// Set the home page from metadata.homeGridId
|
|
744
779
|
if (grdFile.metadata && grdFile.metadata.homeGridId) {
|
|
745
780
|
tree.rootId = grdFile.metadata.homeGridId;
|
|
@@ -1276,6 +1311,7 @@ class AstericsGridProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
1276
1311
|
filename: button.audioRecording.identifier || `audio-${button.id}`,
|
|
1277
1312
|
});
|
|
1278
1313
|
}
|
|
1314
|
+
const locale = tree.metadata?.locale || 'en';
|
|
1279
1315
|
return {
|
|
1280
1316
|
id: button.id,
|
|
1281
1317
|
modelName: 'GridElement',
|
|
@@ -1284,7 +1320,7 @@ class AstericsGridProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
1284
1320
|
height: 1,
|
|
1285
1321
|
x: calculatedX,
|
|
1286
1322
|
y: calculatedY,
|
|
1287
|
-
label: {
|
|
1323
|
+
label: { [locale]: button.label },
|
|
1288
1324
|
wordForms: [],
|
|
1289
1325
|
image: {
|
|
1290
1326
|
data: null,
|
|
@@ -1308,7 +1344,7 @@ class AstericsGridProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
1308
1344
|
id: page.id,
|
|
1309
1345
|
modelName: 'GridData',
|
|
1310
1346
|
modelVersion: '{"major": 5, "minor": 0, "patch": 0}',
|
|
1311
|
-
label: { en: page.name },
|
|
1347
|
+
label: { [tree.metadata?.locale || 'en']: page.name },
|
|
1312
1348
|
rowCount: calculatedRows,
|
|
1313
1349
|
minColumnCount: calculatedCols,
|
|
1314
1350
|
gridElements: gridElements,
|
|
@@ -107,6 +107,7 @@ class DotProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
107
107
|
}
|
|
108
108
|
const { nodes, edges } = this.parseDotFile(content);
|
|
109
109
|
const tree = new treeStructure_1.AACTree();
|
|
110
|
+
tree.metadata.format = 'dot';
|
|
110
111
|
// Create pages for each node and add a self button representing the node label
|
|
111
112
|
for (const node of nodes) {
|
|
112
113
|
const page = new treeStructure_1.AACPage({
|
|
@@ -164,11 +165,14 @@ class DotProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
164
165
|
return resultBuffer;
|
|
165
166
|
}
|
|
166
167
|
saveFromTree(tree, _outputPath) {
|
|
167
|
-
let dotContent =
|
|
168
|
+
let dotContent = `digraph "${tree.metadata?.name || 'AACBoard'}" {\n`;
|
|
168
169
|
// Helper to escape DOT string
|
|
169
170
|
const escapeDotString = (str) => {
|
|
170
171
|
return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
171
172
|
};
|
|
173
|
+
if (tree.metadata?.name) {
|
|
174
|
+
dotContent += ` label="${escapeDotString(tree.metadata.name)}";\n`;
|
|
175
|
+
}
|
|
172
176
|
// Add nodes
|
|
173
177
|
for (const pageId in tree.pages) {
|
|
174
178
|
const page = tree.pages[pageId];
|
|
@@ -54,7 +54,9 @@ class ExcelProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
54
54
|
*/
|
|
55
55
|
loadIntoTree(_filePathOrBuffer) {
|
|
56
56
|
console.warn('ExcelProcessor.loadIntoTree is not implemented yet.');
|
|
57
|
-
|
|
57
|
+
const tree = new treeStructure_1.AACTree();
|
|
58
|
+
tree.metadata.format = 'excel';
|
|
59
|
+
return tree;
|
|
58
60
|
}
|
|
59
61
|
/**
|
|
60
62
|
* Process texts in Excel file (apply translations)
|
|
@@ -486,11 +488,14 @@ class ExcelProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
486
488
|
*/
|
|
487
489
|
async saveFromTreeAsync(tree, outputPath) {
|
|
488
490
|
const workbook = new ExcelJS.Workbook();
|
|
489
|
-
|
|
490
|
-
workbook
|
|
491
|
+
const metadata = tree.metadata;
|
|
492
|
+
// Set workbook properties from tree metadata
|
|
493
|
+
workbook.creator = metadata?.author || 'AACProcessors';
|
|
491
494
|
workbook.lastModifiedBy = 'AACProcessors';
|
|
492
495
|
workbook.created = new Date();
|
|
493
496
|
workbook.modified = new Date();
|
|
497
|
+
workbook.title = metadata?.name || '';
|
|
498
|
+
workbook.subject = metadata?.description || '';
|
|
494
499
|
// If no pages, create a default empty worksheet
|
|
495
500
|
if (Object.keys(tree.pages).length === 0) {
|
|
496
501
|
const worksheet = workbook.addWorksheet('Empty');
|
|
@@ -380,6 +380,12 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
380
380
|
const parser = new fast_xml_parser_1.XMLParser({ ignoreAttributes: false });
|
|
381
381
|
const isEncryptedArchive = typeof filePathOrBuffer === 'string' && filePathOrBuffer.toLowerCase().endsWith('.gridsetx');
|
|
382
382
|
const encryptedContentPassword = this.getGridsetPassword(filePathOrBuffer);
|
|
383
|
+
// Initialize metadata
|
|
384
|
+
const metadata = {
|
|
385
|
+
format: 'gridset',
|
|
386
|
+
isSmartBox: isEncryptedArchive, // SmartBox files are .gridsetx encrypted archives
|
|
387
|
+
passwordProtected: !!password,
|
|
388
|
+
};
|
|
383
389
|
const readEntryBuffer = (entry) => {
|
|
384
390
|
const raw = entry.getData();
|
|
385
391
|
if (!isEncryptedArchive)
|
|
@@ -1321,6 +1327,71 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
1321
1327
|
if (settingsEntry) {
|
|
1322
1328
|
const settingsXml = readEntryBuffer(settingsEntry).toString('utf8');
|
|
1323
1329
|
const settingsData = parser.parse(settingsXml);
|
|
1330
|
+
const gsName = settingsData?.GridSetSettings?.Name ||
|
|
1331
|
+
settingsData?.gridSetSettings?.name ||
|
|
1332
|
+
settingsData?.GridsetSettings?.Name;
|
|
1333
|
+
if (gsName)
|
|
1334
|
+
metadata.name = gsName;
|
|
1335
|
+
const gsDesc = settingsData?.GridSetSettings?.Description ||
|
|
1336
|
+
settingsData?.gridSetSettings?.description ||
|
|
1337
|
+
settingsData?.GridsetSettings?.Description;
|
|
1338
|
+
if (gsDesc)
|
|
1339
|
+
metadata.description = gsDesc;
|
|
1340
|
+
const gsLang = settingsData?.GridSetSettings?.PrimaryLanguage ||
|
|
1341
|
+
settingsData?.gridSetSettings?.primaryLanguage ||
|
|
1342
|
+
settingsData?.GridsetSettings?.PrimaryLanguage;
|
|
1343
|
+
if (gsLang && typeof gsLang === 'string') {
|
|
1344
|
+
metadata.locale = gsLang;
|
|
1345
|
+
metadata.languages = [gsLang];
|
|
1346
|
+
}
|
|
1347
|
+
const gsAuthor = settingsData?.GridSetSettings?.Author ||
|
|
1348
|
+
settingsData?.gridSetSettings?.author ||
|
|
1349
|
+
settingsData?.GridsetSettings?.Author;
|
|
1350
|
+
if (gsAuthor)
|
|
1351
|
+
metadata.author = gsAuthor;
|
|
1352
|
+
const docUrl = settingsData?.GridSetSettings?.DocumentationUrl ||
|
|
1353
|
+
settingsData?.gridSetSettings?.documentationUrl ||
|
|
1354
|
+
settingsData?.GridsetSettings?.DocumentationUrl;
|
|
1355
|
+
if (docUrl) {
|
|
1356
|
+
metadata.homepageUrl = docUrl;
|
|
1357
|
+
metadata.documentationUrl = docUrl;
|
|
1358
|
+
}
|
|
1359
|
+
const docSlug = settingsData?.GridSetSettings?.DocumentationSlug ||
|
|
1360
|
+
settingsData?.gridSetSettings?.documentationSlug ||
|
|
1361
|
+
settingsData?.GridsetSettings?.DocumentationSlug;
|
|
1362
|
+
if (docSlug)
|
|
1363
|
+
metadata.documentationSlug = docSlug;
|
|
1364
|
+
const thumbnail = settingsData?.GridSetSettings?.Thumbnail ||
|
|
1365
|
+
settingsData?.gridSetSettings?.thumbnail ||
|
|
1366
|
+
settingsData?.GridsetSettings?.Thumbnail;
|
|
1367
|
+
if (thumbnail)
|
|
1368
|
+
metadata.thumbnail = thumbnail;
|
|
1369
|
+
const thumbBg = settingsData?.GridSetSettings?.ThumbnailBackground ||
|
|
1370
|
+
settingsData?.gridSetSettings?.thumbnailBackground ||
|
|
1371
|
+
settingsData?.GridsetSettings?.ThumbnailBackground;
|
|
1372
|
+
if (thumbBg)
|
|
1373
|
+
metadata.thumbnailBackground = thumbBg;
|
|
1374
|
+
const picSearchKeys = settingsData?.GridSetSettings?.PictureSearch?.PictureSearchKeys?.PictureSearchKey ||
|
|
1375
|
+
settingsData?.gridSetSettings?.pictureSearch?.pictureSearchKeys?.pictureSearchKey ||
|
|
1376
|
+
settingsData?.GridsetSettings?.PictureSearch?.PictureSearchKeys?.PictureSearchKey;
|
|
1377
|
+
if (picSearchKeys) {
|
|
1378
|
+
metadata.pictureSearchKeys = Array.isArray(picSearchKeys)
|
|
1379
|
+
? picSearchKeys
|
|
1380
|
+
: [picSearchKeys];
|
|
1381
|
+
}
|
|
1382
|
+
const appearance = settingsData?.GridSetSettings?.Appearance ||
|
|
1383
|
+
settingsData?.gridSetSettings?.appearance ||
|
|
1384
|
+
settingsData?.GridsetSettings?.Appearance;
|
|
1385
|
+
if (appearance) {
|
|
1386
|
+
metadata.appearance = {
|
|
1387
|
+
textAtTop: appearance.TextAtTop === '1' ||
|
|
1388
|
+
appearance.textAtTop === '1' ||
|
|
1389
|
+
appearance.TextAtTop === 1,
|
|
1390
|
+
computerControlCellSize: appearance.ComputerControlCellSize
|
|
1391
|
+
? parseFloat(String(appearance.ComputerControlCellSize))
|
|
1392
|
+
: undefined,
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1324
1395
|
const startGridName = settingsData?.GridSetSettings?.StartGrid ||
|
|
1325
1396
|
settingsData?.gridSetSettings?.startGrid ||
|
|
1326
1397
|
settingsData?.GridsetSettings?.StartGrid;
|
|
@@ -1328,19 +1399,22 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
1328
1399
|
// Resolve the grid name to grid ID
|
|
1329
1400
|
const homeGridId = gridNameToIdMap.get(startGridName);
|
|
1330
1401
|
if (homeGridId) {
|
|
1331
|
-
|
|
1402
|
+
metadata.defaultHomePageId = homeGridId;
|
|
1332
1403
|
}
|
|
1333
1404
|
}
|
|
1334
1405
|
const keyboardGridName = settingsData?.GridSetSettings?.KeyboardGrid ||
|
|
1335
|
-
settingsData?.gridSetSettings?.keyboardGrid
|
|
1406
|
+
settingsData?.gridSetSettings?.keyboardGrid ||
|
|
1407
|
+
settingsData?.GridsetSettings?.KeyboardGrid;
|
|
1336
1408
|
if (keyboardGridName && typeof keyboardGridName === 'string') {
|
|
1337
|
-
|
|
1409
|
+
metadata.defaultKeyboardPageId = gridNameToIdMap.get(keyboardGridName);
|
|
1338
1410
|
}
|
|
1339
1411
|
}
|
|
1340
1412
|
}
|
|
1341
1413
|
catch (e) {
|
|
1342
1414
|
// If settings.xml parsing fails, tree.rootId will default to first page
|
|
1343
1415
|
}
|
|
1416
|
+
// Set metadata on tree
|
|
1417
|
+
tree.metadata = metadata;
|
|
1344
1418
|
return tree;
|
|
1345
1419
|
}
|
|
1346
1420
|
processTexts(filePathOrBuffer, translations, outputPath) {
|
|
@@ -202,6 +202,17 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
202
202
|
console.log('[OBF] Detected .obf file, parsed as JSON');
|
|
203
203
|
const page = this.processBoard(boardData, filePathOrBuffer);
|
|
204
204
|
tree.addPage(page);
|
|
205
|
+
// Set metadata from root board
|
|
206
|
+
tree.metadata.format = 'obf';
|
|
207
|
+
tree.metadata.name = boardData.name;
|
|
208
|
+
tree.metadata.description = boardData.description_html;
|
|
209
|
+
tree.metadata.locale = boardData.locale;
|
|
210
|
+
tree.metadata.id = boardData.id;
|
|
211
|
+
if (boardData.url)
|
|
212
|
+
tree.metadata.url = boardData.url;
|
|
213
|
+
if (boardData.locale)
|
|
214
|
+
tree.metadata.languages = [boardData.locale];
|
|
215
|
+
tree.rootId = page.id;
|
|
205
216
|
return tree;
|
|
206
217
|
}
|
|
207
218
|
else {
|
|
@@ -219,6 +230,18 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
219
230
|
console.log('[OBF] Detected buffer/string as OBF JSON');
|
|
220
231
|
const page = this.processBoard(asJson, '[bufferOrString]');
|
|
221
232
|
tree.addPage(page);
|
|
233
|
+
// Set metadata from root board
|
|
234
|
+
tree.metadata.format = 'obf';
|
|
235
|
+
tree.metadata.name = asJson.name;
|
|
236
|
+
tree.metadata.description = asJson.description_html;
|
|
237
|
+
tree.metadata.locale = asJson.locale;
|
|
238
|
+
tree.metadata.id = asJson.id;
|
|
239
|
+
if (asJson.url)
|
|
240
|
+
tree.metadata.url = asJson.url;
|
|
241
|
+
if (asJson.locale) {
|
|
242
|
+
tree.metadata.languages = [asJson.locale];
|
|
243
|
+
}
|
|
244
|
+
tree.rootId = page.id;
|
|
222
245
|
return tree;
|
|
223
246
|
}
|
|
224
247
|
// Otherwise, try as ZIP (.obz). Detect likely zip signature first; throw if neither JSON nor ZIP
|
|
@@ -249,6 +272,19 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
249
272
|
if (boardData) {
|
|
250
273
|
const page = this.processBoard(boardData, entry.entryName);
|
|
251
274
|
tree.addPage(page);
|
|
275
|
+
// Set metadata if not already set (use first board as reference)
|
|
276
|
+
if (!tree.metadata.format) {
|
|
277
|
+
tree.metadata.format = 'obf';
|
|
278
|
+
tree.metadata.name = boardData.name;
|
|
279
|
+
tree.metadata.description = boardData.description_html;
|
|
280
|
+
tree.metadata.locale = boardData.locale;
|
|
281
|
+
tree.metadata.id = boardData.id;
|
|
282
|
+
if (boardData.url)
|
|
283
|
+
tree.metadata.url = boardData.url;
|
|
284
|
+
if (boardData.locale)
|
|
285
|
+
tree.metadata.languages = [boardData.locale];
|
|
286
|
+
tree.rootId = page.id;
|
|
287
|
+
}
|
|
252
288
|
}
|
|
253
289
|
else {
|
|
254
290
|
console.warn('[OBF] Skipped entry (not valid OBF JSON):', entry.entryName);
|
|
@@ -298,15 +334,20 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
298
334
|
}
|
|
299
335
|
return { rows: totalRows, columns: totalColumns, order, buttonPositions };
|
|
300
336
|
}
|
|
301
|
-
createObfBoardFromPage(page, fallbackName) {
|
|
337
|
+
createObfBoardFromPage(page, fallbackName, metadata) {
|
|
302
338
|
const { rows, columns, order, buttonPositions } = this.buildGridMetadata(page);
|
|
303
|
-
const boardName = page.
|
|
339
|
+
const boardName = metadata?.name && page.id === metadata?.defaultHomePageId
|
|
340
|
+
? metadata.name
|
|
341
|
+
: page.name || fallbackName;
|
|
304
342
|
return {
|
|
305
343
|
format: OBF_FORMAT_VERSION,
|
|
306
344
|
id: page.id,
|
|
307
|
-
|
|
345
|
+
url: metadata?.url,
|
|
346
|
+
locale: metadata?.locale || page.locale || 'en',
|
|
308
347
|
name: boardName,
|
|
309
|
-
description_html: page.
|
|
348
|
+
description_html: metadata?.description && page.id === metadata?.defaultHomePageId
|
|
349
|
+
? metadata.description
|
|
350
|
+
: page.descriptionHtml || boardName,
|
|
310
351
|
grid: {
|
|
311
352
|
rows,
|
|
312
353
|
columns,
|
|
@@ -368,14 +409,14 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
368
409
|
if (!rootPage) {
|
|
369
410
|
throw new Error('No pages to save');
|
|
370
411
|
}
|
|
371
|
-
const obfBoard = this.createObfBoardFromPage(rootPage, 'Exported Board');
|
|
412
|
+
const obfBoard = this.createObfBoardFromPage(rootPage, 'Exported Board', tree.metadata);
|
|
372
413
|
fs_1.default.writeFileSync(outputPath, JSON.stringify(obfBoard, null, 2));
|
|
373
414
|
}
|
|
374
415
|
else {
|
|
375
416
|
// Save as OBZ (zip with multiple OBF files)
|
|
376
417
|
const zip = new adm_zip_1.default();
|
|
377
418
|
Object.values(tree.pages).forEach((page) => {
|
|
378
|
-
const obfBoard = this.createObfBoardFromPage(page, 'Board');
|
|
419
|
+
const obfBoard = this.createObfBoardFromPage(page, 'Board', tree.metadata);
|
|
379
420
|
const obfContent = JSON.stringify(obfBoard, null, 2);
|
|
380
421
|
zip.addFile(`${page.id}.obf`, Buffer.from(obfContent, 'utf8'));
|
|
381
422
|
});
|
|
@@ -37,6 +37,7 @@ class ObfsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
37
37
|
*/
|
|
38
38
|
loadIntoTree(filePathOrBuffer) {
|
|
39
39
|
const tree = new treeStructure_1.AACTree();
|
|
40
|
+
tree.metadata.format = 'obfset';
|
|
40
41
|
let content;
|
|
41
42
|
if (Buffer.isBuffer(filePathOrBuffer)) {
|
|
42
43
|
content = filePathOrBuffer.toString('utf-8');
|
|
@@ -109,6 +109,7 @@ class OpmlProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
109
109
|
throw new Error(`Invalid OPML XML: ${e?.message || String(e)}`);
|
|
110
110
|
}
|
|
111
111
|
const tree = new treeStructure_1.AACTree();
|
|
112
|
+
tree.metadata.format = 'opml';
|
|
112
113
|
// Handle case where body.outline might not exist or be in different formats
|
|
113
114
|
const bodyOutline = data.opml?.body?.outline;
|
|
114
115
|
if (!bodyOutline) {
|
|
@@ -49,5 +49,23 @@ declare class SnapProcessor extends BaseProcessor {
|
|
|
49
49
|
* @returns Promise with validation result
|
|
50
50
|
*/
|
|
51
51
|
validate(filePath: string): Promise<ValidationResult>;
|
|
52
|
+
/**
|
|
53
|
+
* Get available PageLayouts for a Snap file
|
|
54
|
+
* Useful for UI components that want to let users select layout size
|
|
55
|
+
* @param filePath - Path to the Snap file
|
|
56
|
+
* @returns Array of available PageLayouts with their dimensions
|
|
57
|
+
*/
|
|
58
|
+
getAvailablePageLayouts(filePath: string): PageLayoutInfo[];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Interface for PageLayout information returned by getAvailablePageLayouts
|
|
62
|
+
*/
|
|
63
|
+
export interface PageLayoutInfo {
|
|
64
|
+
id: number;
|
|
65
|
+
cols: number;
|
|
66
|
+
rows: number;
|
|
67
|
+
size: number;
|
|
68
|
+
hasScanning: boolean;
|
|
69
|
+
label: string;
|
|
52
70
|
}
|
|
53
71
|
export { SnapProcessor };
|
|
@@ -78,16 +78,33 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
78
78
|
let defaultKeyboardPageId;
|
|
79
79
|
let defaultHomePageId;
|
|
80
80
|
let dashboardPageId;
|
|
81
|
+
let toolbarId;
|
|
81
82
|
try {
|
|
82
83
|
const properties = db.prepare('SELECT * FROM PageSetProperties').get();
|
|
83
84
|
if (properties) {
|
|
84
85
|
defaultKeyboardPageId = properties.DefaultKeyboardPageUniqueId;
|
|
85
86
|
defaultHomePageId = properties.DefaultHomePageUniqueId;
|
|
86
87
|
dashboardPageId = properties.DashboardUniqueId;
|
|
87
|
-
|
|
88
|
+
toolbarId = properties.ToolBarUniqueId;
|
|
88
89
|
const hasGlobalToolbar = toolbarId && toolbarId !== '00000000-0000-0000-0000-000000000000';
|
|
90
|
+
// Store metadata in tree
|
|
91
|
+
const metadata = {
|
|
92
|
+
format: 'snap',
|
|
93
|
+
name: properties.Name || properties.PageSetName || undefined,
|
|
94
|
+
description: properties.Description || undefined,
|
|
95
|
+
author: properties.Author || undefined,
|
|
96
|
+
locale: properties.Locale || undefined,
|
|
97
|
+
languages: properties.Locale ? [properties.Locale] : undefined,
|
|
98
|
+
defaultKeyboardPageId: defaultKeyboardPageId || undefined,
|
|
99
|
+
defaultHomePageId: defaultHomePageId || undefined,
|
|
100
|
+
dashboardId: dashboardPageId || undefined,
|
|
101
|
+
hasGlobalToolbar: !!hasGlobalToolbar,
|
|
102
|
+
};
|
|
103
|
+
tree.metadata = metadata;
|
|
104
|
+
// Set toolbarId if there's a global toolbar
|
|
89
105
|
if (hasGlobalToolbar) {
|
|
90
|
-
tree.
|
|
106
|
+
tree.toolbarId = toolbarId || null;
|
|
107
|
+
tree.rootId = toolbarId || defaultHomePageId || null;
|
|
91
108
|
}
|
|
92
109
|
else if (defaultHomePageId) {
|
|
93
110
|
tree.rootId = defaultHomePageId;
|
|
@@ -896,5 +913,74 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
896
913
|
async validate(filePath) {
|
|
897
914
|
return snapValidator_1.SnapValidator.validateFile(filePath);
|
|
898
915
|
}
|
|
916
|
+
/**
|
|
917
|
+
* Get available PageLayouts for a Snap file
|
|
918
|
+
* Useful for UI components that want to let users select layout size
|
|
919
|
+
* @param filePath - Path to the Snap file
|
|
920
|
+
* @returns Array of available PageLayouts with their dimensions
|
|
921
|
+
*/
|
|
922
|
+
getAvailablePageLayouts(filePath) {
|
|
923
|
+
const dbPath = typeof filePath === 'string' ? filePath : path_1.default.join(process.cwd(), 'temp.spb');
|
|
924
|
+
if (Buffer.isBuffer(filePath)) {
|
|
925
|
+
fs_1.default.writeFileSync(dbPath, filePath);
|
|
926
|
+
}
|
|
927
|
+
let db = null;
|
|
928
|
+
try {
|
|
929
|
+
db = new better_sqlite3_1.default(dbPath, { readonly: true });
|
|
930
|
+
// Get unique PageLayouts based on PageLayoutSetting (dimensions)
|
|
931
|
+
const pageLayouts = db
|
|
932
|
+
.prepare(`
|
|
933
|
+
SELECT
|
|
934
|
+
MIN(pl.Id) as Id,
|
|
935
|
+
pl.PageLayoutSetting
|
|
936
|
+
FROM PageLayout pl
|
|
937
|
+
GROUP BY pl.PageLayoutSetting
|
|
938
|
+
ORDER BY pl.PageLayoutSetting
|
|
939
|
+
`)
|
|
940
|
+
.all();
|
|
941
|
+
// Parse the PageLayoutSetting format: "columns,rows,hasScanGroups,?"
|
|
942
|
+
const layouts = pageLayouts.map((pl) => {
|
|
943
|
+
const parts = pl.PageLayoutSetting.split(',');
|
|
944
|
+
const cols = parseInt(parts[0], 10) || 0;
|
|
945
|
+
const rows = parseInt(parts[1], 10) || 0;
|
|
946
|
+
const hasScanning = parts[2] === 'True';
|
|
947
|
+
return {
|
|
948
|
+
id: pl.Id,
|
|
949
|
+
cols,
|
|
950
|
+
rows,
|
|
951
|
+
size: cols * rows,
|
|
952
|
+
hasScanning,
|
|
953
|
+
label: `${cols}×${rows}${hasScanning ? ' (with scanning)' : ''}`,
|
|
954
|
+
};
|
|
955
|
+
});
|
|
956
|
+
// Sort by size (total buttons), with scanning layouts first
|
|
957
|
+
layouts.sort((a, b) => {
|
|
958
|
+
if (a.hasScanning && !b.hasScanning)
|
|
959
|
+
return -1;
|
|
960
|
+
if (!a.hasScanning && b.hasScanning)
|
|
961
|
+
return 1;
|
|
962
|
+
return b.size - a.size; // Larger sizes first
|
|
963
|
+
});
|
|
964
|
+
return layouts;
|
|
965
|
+
}
|
|
966
|
+
catch (error) {
|
|
967
|
+
console.error('[SnapProcessor] Failed to get available page layouts:', error);
|
|
968
|
+
return [];
|
|
969
|
+
}
|
|
970
|
+
finally {
|
|
971
|
+
if (db) {
|
|
972
|
+
db.close();
|
|
973
|
+
}
|
|
974
|
+
// Clean up temporary file if created from buffer
|
|
975
|
+
if (Buffer.isBuffer(filePath) && fs_1.default.existsSync(dbPath)) {
|
|
976
|
+
try {
|
|
977
|
+
fs_1.default.unlinkSync(dbPath);
|
|
978
|
+
}
|
|
979
|
+
catch (e) {
|
|
980
|
+
console.warn('Failed to clean up temporary file:', e);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
899
985
|
}
|
|
900
986
|
exports.SnapProcessor = SnapProcessor;
|
package/dist/types/aac.d.ts
CHANGED
|
@@ -132,14 +132,78 @@ export interface AACPage {
|
|
|
132
132
|
scanningConfig?: ScanningConfig;
|
|
133
133
|
scanBlocksConfig?: any[];
|
|
134
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Generic metadata interface for AAC files
|
|
137
|
+
* All processors can extend this with their specific properties
|
|
138
|
+
*/
|
|
139
|
+
export interface AACTreeMetadata {
|
|
140
|
+
format?: string;
|
|
141
|
+
version?: string;
|
|
142
|
+
locale?: string;
|
|
143
|
+
languages?: string[];
|
|
144
|
+
name?: string;
|
|
145
|
+
description?: string;
|
|
146
|
+
author?: string;
|
|
147
|
+
copyright?: string;
|
|
148
|
+
homepageUrl?: string;
|
|
149
|
+
url?: string;
|
|
150
|
+
id?: string;
|
|
151
|
+
defaultHomePageId?: string;
|
|
152
|
+
defaultKeyboardPageId?: string;
|
|
153
|
+
hasGlobalToolbar?: boolean;
|
|
154
|
+
toolbarId?: string;
|
|
155
|
+
dashboardId?: string;
|
|
156
|
+
[key: string]: any;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Snap-specific metadata
|
|
160
|
+
*/
|
|
161
|
+
export interface SnapMetadata extends AACTreeMetadata {
|
|
162
|
+
format: 'snap';
|
|
163
|
+
dashboardId?: string;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* GridSet-specific metadata
|
|
167
|
+
*/
|
|
168
|
+
export interface GridSetMetadata extends AACTreeMetadata {
|
|
169
|
+
format: 'gridset';
|
|
170
|
+
isSmartBox?: boolean;
|
|
171
|
+
passwordProtected?: boolean;
|
|
172
|
+
pictureSearchKeys?: string[];
|
|
173
|
+
thumbnail?: string;
|
|
174
|
+
thumbnailBackground?: string;
|
|
175
|
+
documentationUrl?: string;
|
|
176
|
+
documentationSlug?: string;
|
|
177
|
+
appearance?: {
|
|
178
|
+
textAtTop?: boolean;
|
|
179
|
+
computerControlCellSize?: number;
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Asterics-specific metadata
|
|
184
|
+
*/
|
|
185
|
+
export interface AstericsGridMetadata extends AACTreeMetadata {
|
|
186
|
+
format: 'asterics';
|
|
187
|
+
hasGlobalGrid?: boolean;
|
|
188
|
+
globalGridId?: string;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* TouchChat-specific metadata
|
|
192
|
+
*/
|
|
193
|
+
export interface TouchChatMetadata extends AACTreeMetadata {
|
|
194
|
+
format: 'touchchat';
|
|
195
|
+
}
|
|
135
196
|
export interface AACTree {
|
|
136
197
|
pages: {
|
|
137
198
|
[key: string]: AACPage;
|
|
138
199
|
};
|
|
200
|
+
metadata: AACTreeMetadata;
|
|
201
|
+
rootId: string | null;
|
|
202
|
+
toolbarId: string | null;
|
|
139
203
|
addPage(page: AACPage): void;
|
|
140
204
|
getPage(id: string): AACPage | undefined;
|
|
141
205
|
}
|
|
142
206
|
export interface AACProcessor {
|
|
143
|
-
extractTexts(filePath: string): string[];
|
|
144
|
-
loadIntoTree(filePath: string): AACTree;
|
|
207
|
+
extractTexts(filePath: string | Buffer): string[];
|
|
208
|
+
loadIntoTree(filePath: string | Buffer): AACTree;
|
|
145
209
|
}
|
|
@@ -35,7 +35,7 @@ class MetricsCalculator {
|
|
|
35
35
|
if (!rootBoard) {
|
|
36
36
|
throw new Error('No root board found in tree');
|
|
37
37
|
}
|
|
38
|
-
this.locale = rootBoard.locale || 'en';
|
|
38
|
+
this.locale = tree.metadata?.locale || rootBoard.locale || 'en';
|
|
39
39
|
// Step 1: Build semantic/clone reference maps
|
|
40
40
|
const { setRefs, setPcts } = this.buildReferenceMaps(tree);
|
|
41
41
|
// Step 2: BFS traversal from root board
|
|
@@ -128,6 +128,9 @@ class MetricsCalculator {
|
|
|
128
128
|
if (options.spellingPageId) {
|
|
129
129
|
spellingPage = tree.getPage(options.spellingPageId) || null;
|
|
130
130
|
}
|
|
131
|
+
if (!spellingPage && tree.metadata?.defaultKeyboardPageId) {
|
|
132
|
+
spellingPage = tree.getPage(tree.metadata.defaultKeyboardPageId) || null;
|
|
133
|
+
}
|
|
131
134
|
if (!spellingPage) {
|
|
132
135
|
// Look for pages with keyboard-like names or content
|
|
133
136
|
spellingPage =
|
|
@@ -17,6 +17,18 @@ export declare class GridsetValidator extends BaseValidator {
|
|
|
17
17
|
* Main validation method
|
|
18
18
|
*/
|
|
19
19
|
validate(content: Buffer | Uint8Array, filename: string, filesize: number): Promise<ValidationResult>;
|
|
20
|
+
/**
|
|
21
|
+
* Check if the buffer is a zip archive
|
|
22
|
+
*/
|
|
23
|
+
private isZip;
|
|
24
|
+
/**
|
|
25
|
+
* Validate a single XML file (legacy or exploded format)
|
|
26
|
+
*/
|
|
27
|
+
private validateSingleXml;
|
|
28
|
+
/**
|
|
29
|
+
* Validate a ZIP archive (.gridset)
|
|
30
|
+
*/
|
|
31
|
+
private validateZipArchive;
|
|
20
32
|
/**
|
|
21
33
|
* Validate Gridset structure
|
|
22
34
|
*/
|
|
@@ -22,6 +22,9 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
22
22
|
__setModuleDefault(result, mod);
|
|
23
23
|
return result;
|
|
24
24
|
};
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
25
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
29
|
exports.GridsetValidator = void 0;
|
|
27
30
|
/* eslint-disable @typescript-eslint/require-await */
|
|
@@ -30,6 +33,7 @@ exports.GridsetValidator = void 0;
|
|
|
30
33
|
const fs = __importStar(require("fs"));
|
|
31
34
|
const path = __importStar(require("path"));
|
|
32
35
|
const xml2js = __importStar(require("xml2js"));
|
|
36
|
+
const adm_zip_1 = __importDefault(require("adm-zip"));
|
|
33
37
|
const baseValidator_1 = require("./baseValidator");
|
|
34
38
|
/**
|
|
35
39
|
* Validator for Grid3/Smartbox Gridset files (.gridset, .gridsetx)
|
|
@@ -86,6 +90,27 @@ class GridsetValidator extends baseValidator_1.BaseValidator {
|
|
|
86
90
|
});
|
|
87
91
|
return this.buildResult(filename, filesize, 'gridset');
|
|
88
92
|
}
|
|
93
|
+
const isZip = this.isZip(content);
|
|
94
|
+
if (isZip) {
|
|
95
|
+
await this.validateZipArchive(content, filename, filesize);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
await this.validateSingleXml(content, filename, filesize);
|
|
99
|
+
}
|
|
100
|
+
return this.buildResult(filename, filesize, 'gridset');
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Check if the buffer is a zip archive
|
|
104
|
+
*/
|
|
105
|
+
isZip(content) {
|
|
106
|
+
if (content.length < 4)
|
|
107
|
+
return false;
|
|
108
|
+
return content[0] === 0x50 && content[1] === 0x4b && content[2] === 0x03 && content[3] === 0x04;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Validate a single XML file (legacy or exploded format)
|
|
112
|
+
*/
|
|
113
|
+
async validateSingleXml(content, filename, _filesize) {
|
|
89
114
|
let xmlObj = null;
|
|
90
115
|
await this.add_check('xml_parse', 'valid XML', async () => {
|
|
91
116
|
try {
|
|
@@ -97,10 +122,8 @@ class GridsetValidator extends baseValidator_1.BaseValidator {
|
|
|
97
122
|
this.err(`Failed to parse XML: ${e.message}`, true);
|
|
98
123
|
}
|
|
99
124
|
});
|
|
100
|
-
if (!xmlObj)
|
|
101
|
-
return
|
|
102
|
-
}
|
|
103
|
-
// eslint-disable-next-line @typescript-eslint/require-await
|
|
125
|
+
if (!xmlObj)
|
|
126
|
+
return;
|
|
104
127
|
await this.add_check('xml_structure', 'gridset root element', async () => {
|
|
105
128
|
if (!xmlObj.gridset && !xmlObj.Gridset) {
|
|
106
129
|
this.err('missing root gridset element', true);
|
|
@@ -110,7 +133,71 @@ class GridsetValidator extends baseValidator_1.BaseValidator {
|
|
|
110
133
|
if (gridset) {
|
|
111
134
|
await this.validateGridsetStructure(gridset, filename, content);
|
|
112
135
|
}
|
|
113
|
-
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Validate a ZIP archive (.gridset)
|
|
139
|
+
*/
|
|
140
|
+
async validateZipArchive(content, filename, _filesize) {
|
|
141
|
+
let zip;
|
|
142
|
+
try {
|
|
143
|
+
zip = new adm_zip_1.default(Buffer.from(content));
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
this.err(`Failed to open ZIP archive: ${e.message}`, true);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const entries = zip.getEntries();
|
|
150
|
+
// Check for gridset.xml (required)
|
|
151
|
+
await this.add_check('gridset_xml_presence', 'gridset.xml presence', async () => {
|
|
152
|
+
const gridsetEntry = entries.find((e) => e.entryName.toLowerCase() === 'gridset.xml');
|
|
153
|
+
if (!gridsetEntry) {
|
|
154
|
+
this.err('Missing gridset.xml in archive', true);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
try {
|
|
158
|
+
const gridsetXml = gridsetEntry.getData().toString('utf-8');
|
|
159
|
+
const parser = new xml2js.Parser();
|
|
160
|
+
const xmlObj = await parser.parseStringPromise(gridsetXml);
|
|
161
|
+
const gridset = xmlObj.gridset || xmlObj.Gridset;
|
|
162
|
+
if (!gridset) {
|
|
163
|
+
this.err('Invalid gridset.xml structure', true);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
await this.validateGridsetStructure(gridset, filename, Buffer.from(gridsetXml));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
this.err(`Failed to parse gridset.xml: ${e.message}`, true);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
// Check for settings.xml (highly recommended/required for metadata)
|
|
175
|
+
await this.add_check('settings_xml_presence', 'settings.xml presence', async () => {
|
|
176
|
+
const settingsEntry = entries.find((e) => e.entryName.toLowerCase() === 'settings.xml');
|
|
177
|
+
if (!settingsEntry) {
|
|
178
|
+
this.warn('Missing settings.xml in archive (required for full metadata)');
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
try {
|
|
182
|
+
const settingsXml = settingsEntry.getData().toString('utf-8');
|
|
183
|
+
const parser = new xml2js.Parser();
|
|
184
|
+
const xmlObj = await parser.parseStringPromise(settingsXml);
|
|
185
|
+
const settings = xmlObj.GridSetSettings || xmlObj.gridSetSettings || xmlObj.GridsetSettings;
|
|
186
|
+
if (!settings) {
|
|
187
|
+
this.warn('Invalid settings.xml structure');
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
// Basic validation of settings.xml
|
|
191
|
+
if (!settings.StartGrid && !settings.startGrid) {
|
|
192
|
+
this.warn('settings.xml missing StartGrid element');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
this.warn(`Failed to parse settings.xml: ${e.message}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
});
|
|
114
201
|
}
|
|
115
202
|
/**
|
|
116
203
|
* Validate Gridset structure
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@willwade/aac-processors",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.20",
|
|
4
4
|
"description": "A comprehensive TypeScript library for processing AAC (Augmentative and Alternative Communication) file formats with translation support",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|