@willwade/aac-processors 0.0.7 → 0.0.8

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.
@@ -1,11 +1,81 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
2
28
  Object.defineProperty(exports, "__esModule", { value: true });
3
29
  exports.getPageTokenImageMap = getPageTokenImageMap;
4
30
  exports.getAllowedImageEntries = getAllowedImageEntries;
5
31
  exports.openImage = openImage;
32
+ exports.findSnapPackages = findSnapPackages;
33
+ exports.findSnapPackagePath = findSnapPackagePath;
34
+ exports.findSnapUsers = findSnapUsers;
35
+ exports.findSnapUserVocabularies = findSnapUserVocabularies;
36
+ exports.findSnapUserHistory = findSnapUserHistory;
37
+ exports.isSnapInstalled = isSnapInstalled;
38
+ exports.readSnapUsage = readSnapUsage;
39
+ exports.readSnapUsageForUser = readSnapUsageForUser;
40
+ const fs = __importStar(require("fs"));
41
+ const path = __importStar(require("path"));
42
+ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
43
+ const dotnetTicks_1 = require("../../utils/dotnetTicks");
6
44
  // Minimal Snap helpers (stubs) to align with processors/<engine>/helpers pattern
7
45
  // NOTE: Snap buttons currently do not populate resolvedImageEntry; these helpers
8
46
  // therefore return empty collections until image resolution is implemented.
47
+ function collectFiles(root, matcher, maxDepth = 3) {
48
+ const results = new Set();
49
+ const stack = [{ dir: root, depth: 0 }];
50
+ while (stack.length > 0) {
51
+ const current = stack.pop();
52
+ if (!current)
53
+ continue;
54
+ if (current.depth > maxDepth)
55
+ continue;
56
+ let entries;
57
+ try {
58
+ entries = fs.readdirSync(current.dir, { withFileTypes: true });
59
+ }
60
+ catch (error) {
61
+ continue;
62
+ }
63
+ for (const entry of entries) {
64
+ const fullPath = path.join(current.dir, entry.name);
65
+ if (entry.isDirectory()) {
66
+ stack.push({ dir: fullPath, depth: current.depth + 1 });
67
+ }
68
+ else if (matcher(fullPath)) {
69
+ results.add(fullPath);
70
+ }
71
+ }
72
+ }
73
+ return Array.from(results);
74
+ }
75
+ /**
76
+ * Build a map of button IDs to resolved image entries for a specific page.
77
+ * Mirrors the Grid helper for consumers that expect image reference data.
78
+ */
9
79
  function getPageTokenImageMap(tree, pageId) {
10
80
  const map = new Map();
11
81
  const page = tree.getPage(pageId);
@@ -17,11 +87,198 @@ function getPageTokenImageMap(tree, pageId) {
17
87
  }
18
88
  return map;
19
89
  }
90
+ /**
91
+ * Collect all image entry paths referenced in a Snap tree.
92
+ * Currently empty until resolvedImageEntry is populated by the processor.
93
+ */
20
94
  function getAllowedImageEntries(_tree) {
21
- // No known image entry paths for Snap yet
22
95
  return new Set();
23
96
  }
97
+ /**
98
+ * Read a binary asset from a Snap pageset.
99
+ * Not implemented yet; provided for API symmetry with other processors.
100
+ */
24
101
  function openImage(_dbOrFile, _entryPath) {
25
- // Not implemented for Snap yet
26
102
  return null;
27
103
  }
104
+ /**
105
+ * Find Tobii Communicator Snap package paths
106
+ * Searches in %LOCALAPPDATA%\Packages for Snap-related packages
107
+ * @param packageNamePattern Optional pattern to filter package names (default: 'TobiiDynavox')
108
+ * @returns Array of Snap package path information
109
+ */
110
+ function findSnapPackages(packageNamePattern = 'TobiiDynavox') {
111
+ const results = [];
112
+ // Only works on Windows
113
+ if (process.platform !== 'win32') {
114
+ return results;
115
+ }
116
+ try {
117
+ const localAppData = process.env.LOCALAPPDATA;
118
+ if (!localAppData) {
119
+ return results;
120
+ }
121
+ const packagesPath = path.join(localAppData, 'Packages');
122
+ // Check if Packages directory exists
123
+ if (!fs.existsSync(packagesPath)) {
124
+ return results;
125
+ }
126
+ // Enumerate packages
127
+ const packages = fs.readdirSync(packagesPath, { withFileTypes: true });
128
+ for (const packageDir of packages) {
129
+ if (!packageDir.isDirectory())
130
+ continue;
131
+ const packageName = packageDir.name;
132
+ // Filter by pattern
133
+ if (packageName.includes(packageNamePattern)) {
134
+ results.push({
135
+ packageName,
136
+ packagePath: path.join(packagesPath, packageName),
137
+ });
138
+ }
139
+ }
140
+ }
141
+ catch (error) {
142
+ // Silently fail if directory access fails
143
+ }
144
+ return results;
145
+ }
146
+ /**
147
+ * Find the first Snap package path matching the pattern
148
+ * Convenience method for when you expect only one Snap installation
149
+ * @param packageNamePattern Optional pattern to filter package names (default: 'TobiiDynavox')
150
+ * @returns Path to the first matching Snap package, or null if not found
151
+ */
152
+ function findSnapPackagePath(packageNamePattern = 'TobiiDynavox') {
153
+ const packages = findSnapPackages(packageNamePattern);
154
+ return packages.length > 0 ? packages[0].packagePath : null;
155
+ }
156
+ /**
157
+ * Find Snap user directories and their vocab files (.sps/.spb)
158
+ * @param packageNamePattern Optional package filter (default TobiiDynavox)
159
+ * @returns Array of user info with vocab paths
160
+ */
161
+ function findSnapUsers(packageNamePattern = 'TobiiDynavox') {
162
+ const results = [];
163
+ if (process.platform !== 'win32') {
164
+ return results;
165
+ }
166
+ const packagePath = findSnapPackagePath(packageNamePattern);
167
+ if (!packagePath) {
168
+ return results;
169
+ }
170
+ const usersRoot = path.join(packagePath, 'LocalState', 'Users');
171
+ if (!fs.existsSync(usersRoot)) {
172
+ return results;
173
+ }
174
+ const entries = fs.readdirSync(usersRoot, { withFileTypes: true });
175
+ for (const entry of entries) {
176
+ if (!entry.isDirectory())
177
+ continue;
178
+ if (entry.name.toLowerCase().startsWith('swiftkey'))
179
+ continue;
180
+ const userPath = path.join(usersRoot, entry.name);
181
+ const vocabPaths = collectFiles(userPath, (full) => {
182
+ const ext = path.extname(full).toLowerCase();
183
+ return ext === '.sps' || ext === '.spb';
184
+ }, 2);
185
+ results.push({
186
+ userId: entry.name,
187
+ userPath,
188
+ vocabPaths,
189
+ });
190
+ }
191
+ return results;
192
+ }
193
+ /**
194
+ * Find vocab files for a specific Snap user (or all users)
195
+ * @param userId Optional user identifier filter (case-sensitive directory name)
196
+ * @param packageNamePattern Optional package filter
197
+ * @returns Array of vocab file paths
198
+ */
199
+ function findSnapUserVocabularies(userId, packageNamePattern = 'TobiiDynavox') {
200
+ const users = findSnapUsers(packageNamePattern).filter((u) => !userId || u.userId === userId);
201
+ return users.flatMap((u) => u.vocabPaths);
202
+ }
203
+ /**
204
+ * Attempt to find history/analytics files for a Snap user by name
205
+ * Currently searches for files containing "history" under the user directory
206
+ * @param userId User identifier (directory name)
207
+ * @param packageNamePattern Optional package filter
208
+ * @returns Array of history file paths (may be empty if not found)
209
+ */
210
+ function findSnapUserHistory(userId, packageNamePattern = 'TobiiDynavox') {
211
+ const user = findSnapUsers(packageNamePattern).find((u) => u.userId === userId);
212
+ if (!user)
213
+ return [];
214
+ return collectFiles(user.userPath, (full) => path.basename(full).toLowerCase().includes('history'), 2);
215
+ }
216
+ /**
217
+ * Check whether TD Snap appears to be installed (Windows only)
218
+ */
219
+ function isSnapInstalled(packageNamePattern = 'TobiiDynavox') {
220
+ if (process.platform !== 'win32')
221
+ return false;
222
+ return Boolean(findSnapPackagePath(packageNamePattern));
223
+ }
224
+ /**
225
+ * Read Snap usage history from a pageset file (.sps/.spb)
226
+ */
227
+ function readSnapUsage(pagesetPath) {
228
+ if (!fs.existsSync(pagesetPath))
229
+ return [];
230
+ const db = new better_sqlite3_1.default(pagesetPath, { readonly: true });
231
+ const tableCheck = db
232
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name IN ('ButtonUsage','Button')")
233
+ .all();
234
+ if (tableCheck.length < 2)
235
+ return [];
236
+ const rows = db
237
+ .prepare(`
238
+ SELECT
239
+ bu.ButtonUniqueId as ButtonId,
240
+ bu.Timestamp as TickValue,
241
+ bu.Modeling as Modeling,
242
+ bu.AccessMethod as AccessMethod,
243
+ b.Label as Label,
244
+ b.Message as Message
245
+ FROM ButtonUsage bu
246
+ LEFT JOIN Button b ON bu.ButtonUniqueId = b.UniqueId
247
+ WHERE bu.Timestamp IS NOT NULL
248
+ ORDER BY bu.Timestamp ASC
249
+ `)
250
+ .all();
251
+ const events = new Map();
252
+ for (const row of rows) {
253
+ const buttonId = row.ButtonId ?? 'unknown';
254
+ const label = row.Label ?? undefined;
255
+ const message = row.Message ?? undefined;
256
+ const content = message || label || '';
257
+ const entry = events.get(buttonId) ??
258
+ {
259
+ id: `snap:${buttonId}`,
260
+ content,
261
+ occurrences: [],
262
+ platform: {
263
+ label,
264
+ message,
265
+ buttonId,
266
+ },
267
+ };
268
+ entry.occurrences.push({
269
+ timestamp: (0, dotnetTicks_1.dotNetTicksToDate)(BigInt(row.TickValue ?? 0)),
270
+ modeling: row.Modeling === 1,
271
+ accessMethod: row.AccessMethod ?? null,
272
+ });
273
+ events.set(buttonId, entry);
274
+ }
275
+ return Array.from(events.values());
276
+ }
277
+ /**
278
+ * Read Snap usage history for a user (all pagesets)
279
+ */
280
+ function readSnapUsageForUser(userId, packageNamePattern = 'TobiiDynavox') {
281
+ const users = findSnapUsers(packageNamePattern).filter((u) => !userId || u.userId === userId);
282
+ const pagesets = users.flatMap((u) => u.vocabPaths);
283
+ return pagesets.flatMap((p) => readSnapUsage(p));
284
+ }
@@ -1,4 +1,16 @@
1
1
  import { AACTree } from '../../core/treeStructure';
2
+ /**
3
+ * Build a map of button IDs to resolved image entry strings for a page.
4
+ * Returns an empty map when no images are present.
5
+ */
2
6
  export declare function getPageTokenImageMap(tree: AACTree, pageId: string): Map<string, string>;
7
+ /**
8
+ * Collect all referenced image entries across the tree.
9
+ * Currently empty until TouchChat image resolution is implemented.
10
+ */
3
11
  export declare function getAllowedImageEntries(_tree: AACTree): Set<string>;
12
+ /**
13
+ * Read a binary asset from a .ce file.
14
+ * Not implemented yet; provided for API symmetry with other processors.
15
+ */
4
16
  export declare function openImage(_ceFile: string | Buffer, _entryPath: string): Buffer | null;
@@ -6,6 +6,10 @@ exports.openImage = openImage;
6
6
  // Minimal TouchChat helpers (stubs) to align with processors/<engine>/helpers pattern
7
7
  // NOTE: TouchChat buttons currently do not populate resolvedImageEntry; these helpers
8
8
  // therefore return empty collections until image resolution is implemented.
9
+ /**
10
+ * Build a map of button IDs to resolved image entry strings for a page.
11
+ * Returns an empty map when no images are present.
12
+ */
9
13
  function getPageTokenImageMap(tree, pageId) {
10
14
  const map = new Map();
11
15
  const page = tree.getPage(pageId);
@@ -17,11 +21,17 @@ function getPageTokenImageMap(tree, pageId) {
17
21
  }
18
22
  return map;
19
23
  }
24
+ /**
25
+ * Collect all referenced image entries across the tree.
26
+ * Currently empty until TouchChat image resolution is implemented.
27
+ */
20
28
  function getAllowedImageEntries(_tree) {
21
- // No known image entry paths for TouchChat yet
22
29
  return new Set();
23
30
  }
31
+ /**
32
+ * Read a binary asset from a .ce file.
33
+ * Not implemented yet; provided for API symmetry with other processors.
34
+ */
24
35
  function openImage(_ceFile, _entryPath) {
25
- // Not implemented for TouchChat yet
26
36
  return null;
27
37
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Number of ticks (.NET 100ns units) between 0001-01-01 and Unix epoch.
3
+ */
4
+ export declare const DOTNET_EPOCH_TICKS = 621355968000000000n;
5
+ /**
6
+ * Number of ticks per millisecond.
7
+ */
8
+ export declare const TICKS_PER_MILLISECOND = 10000n;
9
+ /**
10
+ * Convert .NET ticks (100ns since 0001-01-01) to a JavaScript Date.
11
+ * Accepts bigint or number and rounds down to millisecond precision.
12
+ */
13
+ export declare function dotNetTicksToDate(ticks: number | bigint): Date;
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TICKS_PER_MILLISECOND = exports.DOTNET_EPOCH_TICKS = void 0;
4
+ exports.dotNetTicksToDate = dotNetTicksToDate;
5
+ /**
6
+ * Number of ticks (.NET 100ns units) between 0001-01-01 and Unix epoch.
7
+ */
8
+ exports.DOTNET_EPOCH_TICKS = 621355968000000000n;
9
+ /**
10
+ * Number of ticks per millisecond.
11
+ */
12
+ exports.TICKS_PER_MILLISECOND = 10000n;
13
+ /**
14
+ * Convert .NET ticks (100ns since 0001-01-01) to a JavaScript Date.
15
+ * Accepts bigint or number and rounds down to millisecond precision.
16
+ */
17
+ function dotNetTicksToDate(ticks) {
18
+ const tickValue = BigInt(ticks);
19
+ const ms = Number((tickValue - exports.DOTNET_EPOCH_TICKS) / exports.TICKS_PER_MILLISECOND);
20
+ return new Date(ms);
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@willwade/aac-processors",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
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",
@@ -17,17 +17,18 @@
17
17
  "test": "test"
18
18
  },
19
19
  "scripts": {
20
- "build": "rimraf dist && mkdir dist && ./node_modules/typescript/bin/tsc",
20
+ "build": "rimraf dist && mkdir dist && tsc",
21
21
  "build:watch": "tsc --watch",
22
22
  "clean": "rimraf dist coverage",
23
- "lint": "eslint 'src/**/*.{js,ts}' 'test/**/*.{js,ts}'",
24
- "lint:fix": "eslint 'src/**/*.{js,ts}' 'test/**/*.{js,ts}' --fix",
25
- "format": "prettier --write 'src/**/*.{js,ts}' 'test/**/*.{js,ts}' '*.{js,ts,json,md}'",
26
- "format:check": "prettier --check 'src/**/*.{js,ts}' 'test/**/*.{js,ts}' '*.{js,ts,json,md}'",
23
+ "lint": "eslint \"src/**/*.{js,ts}\" \"test/**/*.{js,ts}\"",
24
+ "lint:fix": "eslint \"src/**/*.{js,ts}\" \"test/**/*.{js,ts}\" --fix",
25
+ "format": "prettier --write \"src/**/*.{js,ts}\" \"test/**/*.{js,ts}\" \"*.{js,ts,json,md}\"",
26
+ "format:check": "prettier --check \"src/**/*.{js,ts}\" \"test/**/*.{js,ts}\" \"*.{js,ts,json,md}\"",
27
27
  "test": "npm run build && jest",
28
28
  "test:watch": "npm run build && jest --watch",
29
29
  "test:coverage": "npm run build && jest --coverage",
30
30
  "test:ci": "npm run build && jest --coverage --ci --watchAll=false --testTimeout=30000",
31
+ "docs": "typedoc",
31
32
  "coverage:report": "node scripts/coverage-analysis.js",
32
33
  "type-check": "tsc --noEmit",
33
34
  "prepublishOnly": "npm run build",
@@ -88,6 +89,7 @@
88
89
  "prettier": "^3.5.3",
89
90
  "rimraf": "^5.0.5",
90
91
  "ts-jest": "^29.1.2",
92
+ "typedoc": "^0.28.14",
91
93
  "typescript": "~5.5.0"
92
94
  },
93
95
  "dependencies": {
package/docs/.keep DELETED
@@ -1 +0,0 @@
1
-