@willwade/aac-processors 0.1.5 → 0.1.6

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.
Files changed (40) hide show
  1. package/README.md +14 -0
  2. package/dist/browser/index.browser.js +15 -1
  3. package/dist/browser/processors/gridset/password.js +11 -0
  4. package/dist/browser/processors/gridsetProcessor.js +42 -46
  5. package/dist/browser/processors/obfProcessor.js +47 -63
  6. package/dist/browser/processors/snapProcessor.js +1031 -0
  7. package/dist/browser/processors/touchchatProcessor.js +1004 -0
  8. package/dist/browser/utils/io.js +20 -0
  9. package/dist/browser/utils/sqlite.js +109 -0
  10. package/dist/browser/utils/zip.js +54 -0
  11. package/dist/browser/validation/snapValidator.js +200 -0
  12. package/dist/browser/validation/touchChatValidator.js +202 -0
  13. package/dist/index.browser.d.ts +7 -0
  14. package/dist/index.browser.js +19 -2
  15. package/dist/processors/gridset/helpers.js +3 -4
  16. package/dist/processors/gridset/index.d.ts +1 -1
  17. package/dist/processors/gridset/index.js +3 -2
  18. package/dist/processors/gridset/password.d.ts +3 -2
  19. package/dist/processors/gridset/password.js +12 -0
  20. package/dist/processors/gridset/wordlistHelpers.js +107 -51
  21. package/dist/processors/gridsetProcessor.js +40 -44
  22. package/dist/processors/obfProcessor.js +46 -62
  23. package/dist/processors/snapProcessor.js +60 -54
  24. package/dist/processors/touchchatProcessor.js +38 -36
  25. package/dist/utils/io.d.ts +2 -0
  26. package/dist/utils/io.js +22 -0
  27. package/dist/utils/sqlite.d.ts +21 -0
  28. package/dist/utils/sqlite.js +137 -0
  29. package/dist/utils/zip.d.ts +7 -0
  30. package/dist/utils/zip.js +80 -0
  31. package/docs/BROWSER_USAGE.md +2 -10
  32. package/examples/README.md +3 -75
  33. package/examples/vitedemo/README.md +13 -7
  34. package/examples/vitedemo/index.html +2 -2
  35. package/examples/vitedemo/package-lock.json +9 -0
  36. package/examples/vitedemo/package.json +1 -0
  37. package/examples/vitedemo/src/main.ts +48 -2
  38. package/examples/vitedemo/src/vite-env.d.ts +1 -0
  39. package/package.json +3 -1
  40. package/examples/browser-test-server.js +0 -81
@@ -1,5 +1,6 @@
1
1
  let cachedFs = null;
2
2
  let cachedPath = null;
3
+ let cachedOs = null;
3
4
  let cachedRequire = undefined;
4
5
  export function getNodeRequire() {
5
6
  if (cachedRequire === undefined) {
@@ -51,6 +52,25 @@ export function getPath() {
51
52
  }
52
53
  return cachedPath;
53
54
  }
55
+ export function getOs() {
56
+ if (!cachedOs) {
57
+ try {
58
+ const nodeRequire = getNodeRequire();
59
+ const osModule = 'os';
60
+ cachedOs = nodeRequire(osModule);
61
+ }
62
+ catch {
63
+ throw new Error('OS utilities are not available in this environment.');
64
+ }
65
+ }
66
+ if (!cachedOs) {
67
+ throw new Error('OS utilities are not available in this environment.');
68
+ }
69
+ return cachedOs;
70
+ }
71
+ export function isNodeRuntime() {
72
+ return typeof process !== 'undefined' && !!process.versions?.node;
73
+ }
54
74
  export function getBasename(filePath) {
55
75
  const parts = filePath.split(/[/\\]/);
56
76
  return parts[parts.length - 1] || filePath;
@@ -0,0 +1,109 @@
1
+ import { getFs, getNodeRequire, getOs, getPath, isNodeRuntime, readBinaryFromInput } from './io';
2
+ let sqlJsConfig = null;
3
+ let sqlJsPromise = null;
4
+ export function configureSqlJs(config) {
5
+ sqlJsConfig = { ...(sqlJsConfig ?? {}), ...config };
6
+ }
7
+ async function getSqlJs() {
8
+ if (!sqlJsPromise) {
9
+ sqlJsPromise = import('sql.js').then((module) => {
10
+ const initSqlJs = module.default || module;
11
+ return initSqlJs(sqlJsConfig ?? {});
12
+ });
13
+ }
14
+ return sqlJsPromise;
15
+ }
16
+ function createSqlJsAdapter(db) {
17
+ return {
18
+ prepare(sql) {
19
+ return {
20
+ all(...params) {
21
+ const stmt = db.prepare(sql);
22
+ if (params.length > 0) {
23
+ stmt.bind(params);
24
+ }
25
+ const rows = [];
26
+ while (stmt.step()) {
27
+ rows.push(stmt.getAsObject());
28
+ }
29
+ stmt.free();
30
+ return rows;
31
+ },
32
+ get(...params) {
33
+ const stmt = db.prepare(sql);
34
+ if (params.length > 0) {
35
+ stmt.bind(params);
36
+ }
37
+ const row = stmt.step() ? stmt.getAsObject() : undefined;
38
+ stmt.free();
39
+ return row;
40
+ },
41
+ run(...params) {
42
+ const stmt = db.prepare(sql);
43
+ if (params.length > 0) {
44
+ stmt.bind(params);
45
+ }
46
+ stmt.step();
47
+ stmt.free();
48
+ return undefined;
49
+ },
50
+ };
51
+ },
52
+ exec(sql) {
53
+ db.exec(sql);
54
+ },
55
+ close() {
56
+ db.close();
57
+ },
58
+ };
59
+ }
60
+ function getBetterSqlite3() {
61
+ try {
62
+ const nodeRequire = getNodeRequire();
63
+ return nodeRequire('better-sqlite3');
64
+ }
65
+ catch {
66
+ throw new Error('better-sqlite3 is not available in this environment.');
67
+ }
68
+ }
69
+ export function requireBetterSqlite3() {
70
+ return getBetterSqlite3();
71
+ }
72
+ export async function openSqliteDatabase(input, options = {}) {
73
+ if (typeof input === 'string') {
74
+ if (!isNodeRuntime()) {
75
+ throw new Error('SQLite file paths are not supported in browser environments.');
76
+ }
77
+ const Database = getBetterSqlite3();
78
+ const db = new Database(input, { readonly: options.readonly ?? true });
79
+ return { db };
80
+ }
81
+ const data = readBinaryFromInput(input);
82
+ if (!isNodeRuntime()) {
83
+ const SQL = await getSqlJs();
84
+ const db = new SQL.Database(data);
85
+ return { db: createSqlJsAdapter(db) };
86
+ }
87
+ const fs = getFs();
88
+ const path = getPath();
89
+ const os = getOs();
90
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aac-sqlite-'));
91
+ const dbPath = path.join(tempDir, 'input.sqlite');
92
+ fs.writeFileSync(dbPath, data);
93
+ const Database = getBetterSqlite3();
94
+ const db = new Database(dbPath, { readonly: options.readonly ?? true });
95
+ const cleanup = () => {
96
+ try {
97
+ db.close();
98
+ }
99
+ finally {
100
+ try {
101
+ fs.rmSync(tempDir, { recursive: true, force: true });
102
+ }
103
+ catch (error) {
104
+ console.warn('Failed to clean up temporary SQLite files:', error);
105
+ }
106
+ }
107
+ };
108
+ return { db, cleanup };
109
+ }
@@ -0,0 +1,54 @@
1
+ import { isNodeRuntime, readBinaryFromInput, getNodeRequire } from './io';
2
+ export async function openZipFromInput(input) {
3
+ if (typeof input === 'string') {
4
+ if (!isNodeRuntime()) {
5
+ throw new Error('Zip file paths are not supported in browser environments.');
6
+ }
7
+ const AdmZip = getNodeRequire()('adm-zip');
8
+ const admZip = new AdmZip(input);
9
+ return {
10
+ zip: {
11
+ listFiles: () => admZip.getEntries().map((entry) => entry.entryName),
12
+ readFile: (name) => {
13
+ const entry = admZip.getEntry(name);
14
+ if (!entry) {
15
+ throw new Error(`Zip entry not found: ${name}`);
16
+ }
17
+ return Promise.resolve(entry.getData());
18
+ },
19
+ },
20
+ };
21
+ }
22
+ const data = readBinaryFromInput(input);
23
+ if (isNodeRuntime()) {
24
+ const AdmZip = getNodeRequire()('adm-zip');
25
+ const admZip = new AdmZip(Buffer.from(data));
26
+ return {
27
+ zip: {
28
+ listFiles: () => admZip.getEntries().map((entry) => entry.entryName),
29
+ readFile: (name) => {
30
+ const entry = admZip.getEntry(name);
31
+ if (!entry) {
32
+ throw new Error(`Zip entry not found: ${name}`);
33
+ }
34
+ return Promise.resolve(entry.getData());
35
+ },
36
+ },
37
+ };
38
+ }
39
+ const module = await import('jszip');
40
+ const init = module.default || module;
41
+ const zip = await init.loadAsync(data);
42
+ return {
43
+ zip: {
44
+ listFiles: () => Object.keys(zip.files),
45
+ readFile: async (name) => {
46
+ const file = zip.file(name);
47
+ if (!file) {
48
+ throw new Error(`Zip entry not found: ${name}`);
49
+ }
50
+ return file.async('uint8array');
51
+ },
52
+ },
53
+ };
54
+ }
@@ -0,0 +1,200 @@
1
+ /* eslint-disable @typescript-eslint/require-await */
2
+ /* eslint-disable @typescript-eslint/no-unsafe-argument */
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import * as xml2js from 'xml2js';
6
+ import JSZip from 'jszip';
7
+ import { BaseValidator } from './baseValidator';
8
+ /**
9
+ * Validator for Snap files (.spb, .sps)
10
+ * Snap files are zipped packages containing XML configuration
11
+ */
12
+ export class SnapValidator extends BaseValidator {
13
+ constructor() {
14
+ super();
15
+ }
16
+ /**
17
+ * Validate a Snap file from disk
18
+ */
19
+ static async validateFile(filePath) {
20
+ const validator = new SnapValidator();
21
+ const content = fs.readFileSync(filePath);
22
+ const stats = fs.statSync(filePath);
23
+ return validator.validate(content, path.basename(filePath), stats.size);
24
+ }
25
+ /**
26
+ * Check if content is Snap format
27
+ */
28
+ // eslint-disable-next-line @typescript-eslint/require-await
29
+ static async identifyFormat(content, filename) {
30
+ const name = filename.toLowerCase();
31
+ if (name.endsWith('.spb') || name.endsWith('.sps')) {
32
+ return true;
33
+ }
34
+ // Try to parse as ZIP and check for Snap structure
35
+ try {
36
+ const buffer = Buffer.isBuffer(content) ? content : Buffer.from(content);
37
+ const zip = await JSZip.loadAsync(buffer);
38
+ const entries = Object.values(zip.files).filter((entry) => !entry.dir);
39
+ return entries.some((entry) => entry.name.includes('settings') || entry.name.includes('.xml'));
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
45
+ /**
46
+ * Main validation method
47
+ */
48
+ async validate(content, filename, filesize) {
49
+ this.reset();
50
+ await this.add_check('filename', 'file extension', async () => {
51
+ if (!filename.match(/\.(spb|sps)$/)) {
52
+ this.warn('filename should end with .spb or .sps');
53
+ }
54
+ });
55
+ let zip = null;
56
+ let validZip = false;
57
+ await this.add_check('zip', 'valid zip package', async () => {
58
+ try {
59
+ const buffer = Buffer.isBuffer(content) ? content : Buffer.from(content);
60
+ zip = await JSZip.loadAsync(buffer);
61
+ const entries = Object.values(zip.files);
62
+ validZip = entries.length > 0;
63
+ }
64
+ catch (e) {
65
+ this.err(`file is not a valid zip package: ${e.message}`, true);
66
+ }
67
+ });
68
+ if (!validZip || !zip) {
69
+ return this.buildResult(filename, filesize, 'snap');
70
+ }
71
+ await this.validateSnapStructure(zip, filename);
72
+ return this.buildResult(filename, filesize, 'snap');
73
+ }
74
+ /**
75
+ * Validate Snap package structure
76
+ */
77
+ async validateSnapStructure(zip, _filename) {
78
+ // Check for required files
79
+ await this.add_check('required_files', 'required package files', async () => {
80
+ const entries = Object.values(zip.files);
81
+ const entryNames = entries.map((e) => e.name);
82
+ // Look for common Snap files
83
+ const hasSettings = entryNames.some((n) => n.toLowerCase().includes('settings'));
84
+ const hasXml = entryNames.some((n) => n.toLowerCase().endsWith('.xml'));
85
+ if (!hasSettings && !hasXml) {
86
+ this.err('Snap package must contain settings.xml or similar configuration file');
87
+ }
88
+ if (entries.length === 0) {
89
+ this.err('Snap package is empty');
90
+ }
91
+ });
92
+ // Try to parse and validate the main settings file
93
+ const settingsEntry = Object.values(zip.files).find((entry) => !entry.dir && entry.name.toLowerCase().includes('settings'));
94
+ if (settingsEntry) {
95
+ await this.validateSettingsFile(settingsEntry);
96
+ }
97
+ // Check for pages
98
+ const pageEntries = Object.values(zip.files).filter((entry) => !entry.dir && entry.name.toLowerCase().includes('page'));
99
+ await this.add_check('pages', 'pages in package', async () => {
100
+ if (pageEntries.length === 0) {
101
+ this.warn('Snap package should contain at least one page file');
102
+ }
103
+ });
104
+ // Validate a sample of pages
105
+ const samplePages = pageEntries.slice(0, 5); // Limit to first 5 pages
106
+ for (let i = 0; i < samplePages.length; i++) {
107
+ await this.validatePageFile(samplePages[i], i);
108
+ }
109
+ // Check for images
110
+ const imageEntries = Object.values(zip.files).filter((entry) => !entry.dir && entry.name.toLowerCase().match(/\.(png|jpg|jpeg|gif|bmp)$/i));
111
+ await this.add_check('images', 'image files', async () => {
112
+ if (imageEntries.length === 0) {
113
+ this.warn('Snap package should contain image files for buttons');
114
+ }
115
+ });
116
+ // Check for audio files
117
+ const audioEntries = Object.values(zip.files).filter((entry) => !entry.dir && entry.name.toLowerCase().match(/\.(wav|mp3|m4a|ogg)$/i));
118
+ await this.add_check('audio', 'audio files', async () => {
119
+ // Audio files are optional, so just warn if missing
120
+ if (audioEntries.length === 0) {
121
+ // This is informational, not a warning
122
+ }
123
+ });
124
+ // Check for unexpected files
125
+ await this.add_check('unexpected_files', 'unexpected file types', async () => {
126
+ const entries = Object.values(zip.files).filter((entry) => !entry.dir);
127
+ const unexpectedFiles = entries.filter((entry) => {
128
+ const name = entry.name.toLowerCase();
129
+ // Skip common system files and directories
130
+ if (name.startsWith('__macosx') || name.startsWith('.ds_store')) {
131
+ return false;
132
+ }
133
+ // Allowed file types
134
+ return !name.match(/\.(xml|png|jpg|jpeg|gif|bmp|wav|mp3|m4a|ogg|json)$/i);
135
+ });
136
+ if (unexpectedFiles.length > 0) {
137
+ const unexpectedNames = unexpectedFiles.map((f) => f.name).slice(0, 5);
138
+ this.warn(`Package contains unexpected file types: ${unexpectedNames.join(', ')}`);
139
+ }
140
+ });
141
+ }
142
+ /**
143
+ * Validate the main settings file
144
+ */
145
+ async validateSettingsFile(entry) {
146
+ await this.add_check('settings_format', 'settings file format', async () => {
147
+ try {
148
+ const content = await entry.async('string');
149
+ const parser = new xml2js.Parser();
150
+ const xml = await parser.parseStringPromise(content);
151
+ // Check for expected root element
152
+ if (!xml.settings && !xml.Settings && !xml.page && !xml.Page) {
153
+ this.warn('settings file does not contain expected root element');
154
+ }
155
+ // Check for required settings attributes if present
156
+ const settings = xml.settings || xml.Settings;
157
+ if (settings) {
158
+ const id = settings.$?.id || settings.$?.Id;
159
+ const name = settings.$?.name || settings.$?.Name;
160
+ if (!id && !name) {
161
+ this.warn('settings should have an id or name attribute');
162
+ }
163
+ }
164
+ }
165
+ catch (e) {
166
+ this.err(`Failed to parse settings file: ${e.message}`);
167
+ }
168
+ });
169
+ }
170
+ /**
171
+ * Validate a page file
172
+ */
173
+ async validatePageFile(entry, index) {
174
+ await this.add_check(`page[${index}]`, `page file ${index}: ${entry.name}`, async () => {
175
+ try {
176
+ const content = await entry.async('string');
177
+ const parser = new xml2js.Parser();
178
+ const xml = await parser.parseStringPromise(content);
179
+ const page = xml.page || xml.Page;
180
+ if (!page) {
181
+ this.err(`Page file ${entry.name} does not contain a page element`);
182
+ return;
183
+ }
184
+ // Check page attributes
185
+ const pageId = page.$?.id || page.$?.Id;
186
+ if (!pageId) {
187
+ this.warn(`Page ${entry.name} is missing an id attribute`);
188
+ }
189
+ // Check for cells/buttons
190
+ const cells = page.cells || page.Cells || page.button || page.Button;
191
+ if (!cells || (Array.isArray(cells) && cells.length === 0)) {
192
+ this.warn(`Page ${entry.name} has no cells or buttons`);
193
+ }
194
+ }
195
+ catch (e) {
196
+ this.err(`Failed to parse page file ${entry.name}: ${e.message}`);
197
+ }
198
+ });
199
+ }
200
+ }
@@ -0,0 +1,202 @@
1
+ /* eslint-disable @typescript-eslint/require-await */
2
+ /* eslint-disable @typescript-eslint/no-unsafe-argument */
3
+ /* eslint-disable @typescript-eslint/no-unsafe-return */
4
+ import * as fs from 'fs';
5
+ import * as path from 'path';
6
+ import * as xml2js from 'xml2js';
7
+ import { BaseValidator } from './baseValidator';
8
+ /**
9
+ * Validator for TouchChat files (.ce)
10
+ * TouchChat files are XML-based
11
+ */
12
+ export class TouchChatValidator extends BaseValidator {
13
+ constructor() {
14
+ super();
15
+ }
16
+ /**
17
+ * Validate a TouchChat file from disk
18
+ */
19
+ static async validateFile(filePath) {
20
+ const validator = new TouchChatValidator();
21
+ const content = fs.readFileSync(filePath);
22
+ const stats = fs.statSync(filePath);
23
+ return validator.validate(content, path.basename(filePath), stats.size);
24
+ }
25
+ /**
26
+ * Check if content is TouchChat format
27
+ */
28
+ static async identifyFormat(content, filename) {
29
+ const name = filename.toLowerCase();
30
+ if (name.endsWith('.ce')) {
31
+ return true;
32
+ }
33
+ // Try to parse as XML and check for TouchChat structure
34
+ try {
35
+ const contentStr = Buffer.isBuffer(content) ? content.toString('utf-8') : content;
36
+ const parser = new xml2js.Parser();
37
+ const result = await parser.parseStringPromise(contentStr);
38
+ // TouchChat files typically have specific structure
39
+ return result && (result.PageSet || result.Pageset || result.page || result.Page);
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
45
+ /**
46
+ * Main validation method
47
+ */
48
+ async validate(content, filename, filesize) {
49
+ this.reset();
50
+ await this.add_check('filename', 'file extension', async () => {
51
+ if (!filename.match(/\.ce$/i)) {
52
+ this.warn('filename should end with .ce');
53
+ }
54
+ });
55
+ let xmlObj = null;
56
+ await this.add_check('xml_parse', 'valid XML', async () => {
57
+ try {
58
+ const parser = new xml2js.Parser();
59
+ const contentStr = content.toString('utf-8');
60
+ xmlObj = await parser.parseStringPromise(contentStr);
61
+ }
62
+ catch (e) {
63
+ this.err(`Failed to parse XML: ${e.message}`, true);
64
+ }
65
+ });
66
+ if (!xmlObj) {
67
+ return this.buildResult(filename, filesize, 'touchchat');
68
+ }
69
+ await this.add_check('xml_structure', 'TouchChat root element', async () => {
70
+ // TouchChat can have different root elements
71
+ const hasValidRoot = xmlObj.PageSet ||
72
+ xmlObj.Pageset ||
73
+ xmlObj.page ||
74
+ xmlObj.Page ||
75
+ xmlObj.pages ||
76
+ xmlObj.Pages;
77
+ if (!hasValidRoot) {
78
+ this.err('file does not contain a recognized TouchChat structure');
79
+ }
80
+ });
81
+ const root = xmlObj.PageSet ||
82
+ xmlObj.Pageset ||
83
+ xmlObj.page ||
84
+ xmlObj.Page ||
85
+ xmlObj.pages ||
86
+ xmlObj.Pages;
87
+ if (root) {
88
+ await this.validateTouchChatStructure(root);
89
+ }
90
+ return this.buildResult(filename, filesize, 'touchchat');
91
+ }
92
+ /**
93
+ * Validate TouchChat structure
94
+ */
95
+ async validateTouchChatStructure(root) {
96
+ // Check for ID
97
+ await this.add_check('root_id', 'root element ID', async () => {
98
+ const id = root.$?.id || root.$?.Id;
99
+ if (!id) {
100
+ this.warn('root element should have an id attribute');
101
+ }
102
+ });
103
+ // Check for name
104
+ await this.add_check('root_name', 'root element name', async () => {
105
+ const name = root.$?.name || root.$?.Name || root.name?.[0];
106
+ if (!name) {
107
+ this.warn('root element should have a name');
108
+ }
109
+ });
110
+ // Check for pages
111
+ await this.add_check('pages', 'pages collection', async () => {
112
+ const pages = root.page || root.Page || root.pages || root.Pages;
113
+ if (!pages) {
114
+ this.err('TouchChat file must contain pages');
115
+ }
116
+ else if (!Array.isArray(pages) || pages.length === 0) {
117
+ this.err('TouchChat file must contain at least one page');
118
+ }
119
+ });
120
+ // Validate individual pages
121
+ const pages = root.page || root.Page || root.pages || root.Pages;
122
+ if (pages && Array.isArray(pages)) {
123
+ await this.add_check('page_count', 'page count', async () => {
124
+ if (pages.length === 0) {
125
+ this.err('Must contain at least one page');
126
+ }
127
+ });
128
+ // Sample first few pages
129
+ const sampleSize = Math.min(pages.length, 5);
130
+ for (let i = 0; i < sampleSize; i++) {
131
+ await this.validatePage(pages[i], i);
132
+ }
133
+ }
134
+ }
135
+ /**
136
+ * Validate a single page
137
+ */
138
+ async validatePage(page, index) {
139
+ await this.add_check(`page[${index}]_id`, `page ${index} ID`, async () => {
140
+ const id = page.$?.id || page.$?.Id;
141
+ if (!id) {
142
+ this.warn(`page ${index} is missing an id attribute`);
143
+ }
144
+ });
145
+ await this.add_check(`page[${index}]_name`, `page ${index} name`, async () => {
146
+ const name = page.$?.name || page.$?.Name || page.name?.[0];
147
+ if (!name) {
148
+ this.warn(`page ${index} should have a name`);
149
+ }
150
+ });
151
+ // Check for buttons/items
152
+ await this.add_check(`page[${index}]_buttons`, `page ${index} buttons`, async () => {
153
+ const buttons = page.button || page.Button || page.item || page.Item;
154
+ if (!buttons) {
155
+ this.warn(`page ${index} has no buttons/items`);
156
+ }
157
+ else if (Array.isArray(buttons) && buttons.length === 0) {
158
+ this.warn(`page ${index} should contain at least one button`);
159
+ }
160
+ });
161
+ // Validate button references
162
+ const buttons = page.button || page.Button || page.item || page.Item;
163
+ if (buttons && Array.isArray(buttons)) {
164
+ const sampleSize = Math.min(buttons.length, 3);
165
+ for (let i = 0; i < sampleSize; i++) {
166
+ await this.validateButton(buttons[i], index, i);
167
+ }
168
+ }
169
+ }
170
+ /**
171
+ * Validate a single button
172
+ */
173
+ async validateButton(button, pageIdx, buttonIdx) {
174
+ await this.add_check(`page[${pageIdx}]_button[${buttonIdx}]_label`, `button label`, async () => {
175
+ const label = button.$?.label || button.$?.Label || button.label?.[0];
176
+ if (!label) {
177
+ this.warn(`button ${buttonIdx} on page ${pageIdx} should have a label`);
178
+ }
179
+ });
180
+ await this.add_check(`page[${pageIdx}]_button[${buttonIdx}]_vocalization`, `button vocalization`, async () => {
181
+ const vocalization = button.$?.vocalization || button.$?.Vocalization || button.vocalization?.[0];
182
+ if (!vocalization) {
183
+ // Vocalization is optional, so just info
184
+ }
185
+ });
186
+ // Check for image reference
187
+ await this.add_check(`page[${pageIdx}]_button[${buttonIdx}]_image`, `button image`, async () => {
188
+ const image = button.$?.image || button.$?.Image || button.img?.[0];
189
+ if (!image) {
190
+ this.warn(`button ${buttonIdx} on page ${pageIdx} should have an image reference`);
191
+ }
192
+ });
193
+ // Check for link/action
194
+ await this.add_check(`page[${pageIdx}]_button[${buttonIdx}]_action`, `button action`, async () => {
195
+ const link = button.$?.link || button.$?.Link;
196
+ const action = button.$?.action || button.$?.Action;
197
+ if (!link && !action) {
198
+ // Not all buttons need actions, they can just speak
199
+ }
200
+ });
201
+ }
202
+ }
@@ -6,6 +6,10 @@
6
6
  * **NOTE: Gridset .gridsetx files**
7
7
  * GridsetProcessor supports regular `.gridset` files in browser.
8
8
  * Encrypted `.gridsetx` files require Node.js for crypto operations and are not supported in browser.
9
+ *
10
+ * **NOTE: SQLite-backed formats**
11
+ * Snap (.sps/.spb) and TouchChat (.ce) require a WASM-backed SQLite engine.
12
+ * Configure `sql.js` in browser builds via `configureSqlJs()` before loading these formats.
9
13
  */
10
14
  export * from './core/treeStructure';
11
15
  export * from './core/baseProcessor';
@@ -14,9 +18,12 @@ export { DotProcessor } from './processors/dotProcessor';
14
18
  export { OpmlProcessor } from './processors/opmlProcessor';
15
19
  export { ObfProcessor } from './processors/obfProcessor';
16
20
  export { GridsetProcessor } from './processors/gridsetProcessor';
21
+ export { SnapProcessor } from './processors/snapProcessor';
22
+ export { TouchChatProcessor } from './processors/touchchatProcessor';
17
23
  export { ApplePanelsProcessor } from './processors/applePanelsProcessor';
18
24
  export { AstericsGridProcessor } from './processors/astericsGridProcessor';
19
25
  import { BaseProcessor } from './core/baseProcessor';
26
+ export { configureSqlJs } from './utils/sqlite';
20
27
  /**
21
28
  * Factory function to get the appropriate processor for a file extension
22
29
  * @param filePathOrExtension - File path or extension (e.g., '.dot', '/path/to/file.obf')