diff --git a/.eslintrc.json b/.eslintrc.json index 92b12d9..16d5860 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -82,10 +82,7 @@ "parserOptions": { "project": "./tsconfig.test.json" }, - "extends": [ - "plugin:@typescript-eslint/recommended", - "plugin:prettier/recommended" - ], + "extends": ["plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"], "rules": { "@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/no-explicit-any": "off", diff --git a/.prettierrc b/.prettierrc index 168d9d2..a1ab6bb 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,3 +1,6 @@ { - "endOfLine": "auto" + "endOfLine": "auto", + "singleQuote": true, + "trailingComma": "es5", + "printWidth": 100 } diff --git a/CHANGELOG.md b/CHANGELOG.md index a5f2b93..7cd7540 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,10 +47,10 @@ await processor.processLLMTranslations(file, translations, output); ```typescript // Before -const result = analyze(file, format); // Returns { tree } +const result = analyze(file, format); // Returns { tree } // After -const result = await analyze(file, format); // Returns Promise<{ tree }> +const result = await analyze(file, format); // Returns Promise<{ tree }> ``` ### Migration Guide @@ -154,6 +154,7 @@ async function processMultipleFiles(files: string[]) { ### Browser Compatibility Progress This change enables the following browser-compatible processors: + - ✅ DotProcessor - ✅ OpmlProcessor - ✅ ObfProcessor (JSZip migration complete!) @@ -164,6 +165,7 @@ This change enables the following browser-compatible processors: **Note:** Gridset `.gridsetx` encrypted files require Node.js for crypto operations. Regular `.gridset` files work in browser. Still Node-only (deferred): + - ❌ SnapProcessor (sqlite - needs wasm sqlite) - ❌ TouchChatProcessor (sqlite - needs wasm sqlite) - ❌ ExcelProcessor (fs dependencies - needs audit) @@ -314,10 +316,10 @@ npm install aac-processors@2.x ```javascript // Old (CommonJS) -const { DotProcessor } = require("aac-processors/dist/processors"); +const { DotProcessor } = require('aac-processors/dist/processors'); // New (ES Modules + TypeScript) -import { DotProcessor, getProcessor } from "aac-processors"; +import { DotProcessor, getProcessor } from 'aac-processors'; ``` ### API Changes @@ -325,23 +327,23 @@ import { DotProcessor, getProcessor } from "aac-processors"; ```typescript // Old const processor = new DotProcessor(); -const tree = processor.loadIntoTree("file.dot"); +const tree = processor.loadIntoTree('file.dot'); // New (same API, but with TypeScript support) const processor = new DotProcessor(); -const tree: AACTree = processor.loadIntoTree("file.dot"); +const tree: AACTree = processor.loadIntoTree('file.dot'); // New factory pattern -const processor = getProcessor("file.dot"); // Auto-detects format +const processor = getProcessor('file.dot'); // Auto-detects format ``` ### New Translation Workflow ```typescript // New in 2.x -const texts = processor.extractTexts("file.dot"); -const translations = new Map([["Hello", "Hola"]]); -const result = processor.processTexts("file.dot", translations, "output.dot"); +const texts = processor.extractTexts('file.dot'); +const translations = new Map([['Hello', 'Hola']]); +const result = processor.processTexts('file.dot', translations, 'output.dot'); ``` For detailed migration assistance, see the [Migration Guide](docs/MIGRATION.md). diff --git a/README.md b/README.md index dafd579..38f0ba2 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ npm install @willwade/aac-processors ## Dual Build Targets ### Node.js (default) + Full feature set, including filesystem access, SQLite-backed formats, and ZIP/encrypted formats. @@ -27,6 +28,7 @@ const texts = await snap.extractTexts('board.sps'); ``` ### Browser + Browser-safe entry that avoids Node-only dependencies. It expects `Buffer`, `Uint8Array`, or `ArrayBuffer` inputs rather than file paths. @@ -82,7 +84,8 @@ const translations = new Map([ await processor.processTexts('board.dot', translations, 'board-es.dot'); ``` -NB: Please use [https://aactools.co.uk](https://aactools.co.uk) for a far more comphrensive translation logic - where we do far far more than this... + +NB: Please use [https://aactools.co.uk](https://aactools.co.uk) for a far more comphrensive translation logic - where we do far far more than this... ## Documentation diff --git a/jest.config.js b/jest.config.js index e969e51..129cf84 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,42 +1,31 @@ /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { rootDir: __dirname, - preset: "ts-jest", - testEnvironment: "node", - roots: ["/src", "/test"], - testMatch: [ - "**/__tests__/**/*.+(ts|tsx|js)", - "**/?(*.)+(spec|test).+(ts|tsx|js)", - ], + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/src', '/test'], + testMatch: ['**/__tests__/**/*.+(ts|tsx|js)', '**/?(*.)+(spec|test).+(ts|tsx|js)'], transform: { - "^.+\\.(ts|tsx)$": [ - "ts-jest", + '^.+\\.(ts|tsx)$': [ + 'ts-jest', { - tsconfig: "/tsconfig.test.json", + tsconfig: '/tsconfig.test.json', }, ], }, - moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], collectCoverage: true, - coverageDirectory: "coverage", - coverageReporters: [ - "text", - "text-summary", - "lcov", - "html", - "json", - "json-summary", - "cobertura", - ], + coverageDirectory: 'coverage', + coverageReporters: ['text', 'text-summary', 'lcov', 'html', 'json', 'json-summary', 'cobertura'], collectCoverageFrom: [ - "src/**/*.{js,ts}", - "!src/**/*.d.ts", - "!src/**/*.test.{js,ts}", - "!src/**/__tests__/**", - "!src/cli/**", - "!src/utilities/**", + 'src/**/*.{js,ts}', + '!src/**/*.d.ts', + '!src/**/*.test.{js,ts}', + '!src/**/__tests__/**', + '!src/cli/**', + '!src/utilities/**', ], - coveragePathIgnorePatterns: ["/node_modules/", "/dist/", "/__tests__/"], + coveragePathIgnorePatterns: ['/node_modules/', '/dist/', '/__tests__/'], coverageThreshold: { global: { branches: 58, @@ -45,13 +34,13 @@ module.exports = { statements: 72, }, // Per-file thresholds for critical components - "src/core/": { + 'src/core/': { branches: 80, functions: 88, lines: 88, statements: 88, }, - "src/processors/dotProcessor.ts": { + 'src/processors/dotProcessor.ts': { branches: 70, functions: 80, lines: 80, @@ -60,8 +49,8 @@ module.exports = { }, // Enable module resolution for both src and dist moduleNameMapper: { - "^@/(.*)$": "/src/$1", + '^@/(.*)$': '/src/$1', }, // Ensure Jest can find modules - moduleDirectories: ["node_modules", "/src", "/dist"], + moduleDirectories: ['node_modules', '/src', '/dist'], }; diff --git a/src/processors/gridset/cellHelpers.ts b/src/processors/gridset/cellHelpers.ts new file mode 100644 index 0000000..c784867 --- /dev/null +++ b/src/processors/gridset/cellHelpers.ts @@ -0,0 +1,122 @@ +/** + * Grid3 Cell Helpers + * + * Utilities for working with Grid 3 cells, including finding button positions + * and calculating cell spans in grid layouts. + */ + +import type { AACPage, AACButton } from '../../core/treeStructure'; + +/** + * Cell position with span information + */ +export interface CellPosition { + /** X coordinate (column) */ + x: number; + /** Y coordinate (row) */ + y: number; + /** Number of columns the cell spans */ + columnSpan: number; + /** Number of rows the cell spans */ + rowSpan: number; +} + +/** + * Find button position with span information + * + * Searches the page's grid layout for a button and calculates its position + * and span (how many columns/rows it occupies). + * + * @param page - The AAC page containing the button + * @param button - The button to locate + * @param fallbackIndex - Index to use if button not found in grid + * @returns Position and span information for the button + * + * @example + * const position = findButtonPosition(page, button, 0); + * console.log(`Button at ${position.x},${position.y} spans ${position.columnSpan}x${position.rowSpan}`); + */ +export function findButtonPosition( + page: AACPage, + button: AACButton, + fallbackIndex: number +): CellPosition { + if (page.grid && page.grid.length > 0) { + // Search for button in grid layout and calculate span + for (let y = 0; y < page.grid.length; y++) { + for (let x = 0; x < page.grid[y].length; x++) { + const current = page.grid[y][x]; + if (current && current.id === button.id) { + // Calculate span by checking how far the same button extends + let columnSpan = 1; + let rowSpan = 1; + + // Check column span (rightward) + while (x + columnSpan < page.grid[y].length) { + const right = page.grid[y][x + columnSpan]; + if (right && right.id === button.id) { + columnSpan++; + } else { + break; + } + } + + // Check row span (downward) + while (y + rowSpan < page.grid.length) { + const below = page.grid[y + rowSpan][x]; + if (below && below.id === button.id) { + rowSpan++; + } else { + break; + } + } + + return { x, y, columnSpan, rowSpan }; + } + } + } + } + + // Fallback positioning + const gridCols = page.grid?.[0]?.length || Math.ceil(Math.sqrt(page.buttons.length)); + return { + x: fallbackIndex % gridCols, + y: Math.floor(fallbackIndex / gridCols), + columnSpan: 1, + rowSpan: 1, + }; +} + +/** + * Calculate cell position key for Maps and Sets + * + * Creates a string key from X and Y coordinates for use as a Map key or Set entry. + * + * @param x - X coordinate + * @param y - Y coordinate + * @returns String key in format "x,y" + * + * @example + * const key = cellPositionKey(5, 3); + * console.log(key); // "5,3" + */ +export function cellPositionKey(x: number, y: number): string { + return `${x},${y}`; +} + +/** + * Parse cell position key + * + * Extracts X and Y coordinates from a position key string. + * + * @param key - Position key in format "x,y" + * @returns Object with x and y properties + * + * @example + * const pos = parseCellPositionKey("5,3"); + * console.log(pos); // { x: 5, y: 3 } + */ +export function parseCellPositionKey(key: string): { x: number; y: number } { + const [x, y] = key.split(',').map(Number); + return { x, y }; +} diff --git a/src/processors/gridset/gridCalculations.ts b/src/processors/gridset/gridCalculations.ts new file mode 100644 index 0000000..0207cef --- /dev/null +++ b/src/processors/gridset/gridCalculations.ts @@ -0,0 +1,78 @@ +/** + * Grid3 Grid Calculations + * + * Utilities for calculating grid dimensions and definitions + * based on page layout and button count. + */ + +import type { AACPage } from '../../core/treeStructure'; + +/** + * Grid definition structure for Grid 3 XML + */ +export interface GridDefinitions { + ColumnDefinition: any[]; +} + +export interface RowDefinitions { + RowDefinition: any[]; +} + +/** + * Calculate column definitions based on page layout + * + * Analyzes the page's grid structure to determine the number of columns. + * If no grid exists, estimates from button count. + * + * @param page - The AAC page to analyze + * @returns Column definitions object for Grid 3 XML + * + * @example + * const columns = calculateColumnDefinitions(page); + * // Returns: { ColumnDefinition: [{}, {}, {}, {}] } for 4 columns + */ +export function calculateColumnDefinitions(page: AACPage): GridDefinitions { + let maxCols = 4; // Default minimum + + if (page.grid && page.grid.length > 0) { + maxCols = Math.max(maxCols, page.grid[0]?.length || 0); + } else { + // Fallback: estimate from button count + maxCols = Math.max(4, Math.ceil(Math.sqrt(page.buttons.length))); + } + + return { + ColumnDefinition: Array(maxCols).fill({}), + }; +} + +/** + * Calculate row definitions based on page layout + * + * Analyzes the page's grid structure to determine the number of rows. + * If no grid exists, estimates from button count. + * + * @param page - The AAC page to analyze + * @param addWorkspaceOffset - Whether to add 1 row for workspace (default: false) + * @returns Row definitions object for Grid 3 XML + * + * @example + * const rows = calculateRowDefinitions(page, false); + * // Returns: { RowDefinition: [{}, {}, {}, {}] } for 4 rows + */ +export function calculateRowDefinitions(page: AACPage, addWorkspaceOffset = false): RowDefinitions { + let maxRows = 4; // Default minimum + const offset = addWorkspaceOffset ? 1 : 0; + + if (page.grid && page.grid.length > 0) { + maxRows = Math.max(maxRows, page.grid.length + offset); + } else { + // Fallback: estimate from button count + const estimatedCols = Math.ceil(Math.sqrt(page.buttons.length)); + maxRows = Math.max(4, Math.ceil(page.buttons.length / estimatedCols)) + offset; + } + + return { + RowDefinition: Array(maxRows).fill({}), + }; +} diff --git a/src/processors/gridset/helpers.ts b/src/processors/gridset/helpers.ts index 9f00bda..e1462d6 100644 --- a/src/processors/gridset/helpers.ts +++ b/src/processors/gridset/helpers.ts @@ -219,7 +219,10 @@ export function getCommonDocumentsPath(): string { const child_process = getNodeRequire()('child_process') as typeof import('child_process'); const command = 'REG.EXE QUERY "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders" /V "Common Documents"'; - const output = child_process.execSync(command, { encoding: 'utf-8', windowsHide: true }); + const output = child_process.execSync(command, { + encoding: 'utf-8', + windowsHide: true, + }); // Parse the output to extract the path const match = output.match(/Common Documents\s+REG_SZ\s+(.+)/); diff --git a/src/processors/gridset/wordlistHelpers.ts b/src/processors/gridset/wordlistHelpers.ts index 4bd8fb2..e964c9e 100644 --- a/src/processors/gridset/wordlistHelpers.ts +++ b/src/processors/gridset/wordlistHelpers.ts @@ -89,9 +89,10 @@ export function wordlistToXml(wordlist: WordList): string { const items = wordlist.items.map((item) => ({ WordListItem: { Text: { - s: { - '@_Image': item.image || '', - r: item.text, + p: { + s: { + r: item.text, + }, }, }, Image: item.image || '', @@ -174,7 +175,8 @@ export async function extractWordlists( : []; const items: WordListItem[] = itemArray.map((item: any) => ({ - text: item.Text?.s?.r || item.text?.s?.r || '', + text: + item.Text?.p?.s?.r || item.Text?.s?.r || item.text?.p?.s?.r || item.text?.s?.r || '', image: item.Image || item.image || undefined, partOfSpeech: item.PartOfSpeech || item.partOfSpeech || 'Unknown', })); @@ -253,9 +255,10 @@ export async function updateWordlist( const items = wordlist.items.map((item) => ({ WordListItem: { Text: { - s: { - '@_Image': item.image || '', - r: item.text, + p: { + s: { + r: item.text, + }, }, }, Image: item.image || '', diff --git a/src/processors/gridset/xmlFormatter.ts b/src/processors/gridset/xmlFormatter.ts new file mode 100644 index 0000000..13d8221 --- /dev/null +++ b/src/processors/gridset/xmlFormatter.ts @@ -0,0 +1,96 @@ +/** + * Grid3 XML Formatter + * + * Utilities for formatting XML to match Grid 3's specific requirements. + * Grid 3 has strict formatting requirements including line endings, self-closing + * tag spacing, and specific tag expansion rules. + */ + +/** + * Tags that Grid 3 requires in full opening/closing format instead of self-closing + * Grid 3 cannot parse - it requires + */ +const TAGS_NEEDING_EXPANSION = ['AudioDescription', 'VideoDescription']; + +/** + * Format XML string to match Grid 3's requirements + * + * Grid 3 requires specific formatting: + * - Windows line endings (\r\n) + * - Space before /> in self-closing tags: not + * - Plain apostrophes instead of ' + * - Specific tags expanded to full opening/closing format + * - CDATA for empty/whitespace captions and tags + * + * @param xml - The XML string to format + * @returns Formatted XML string compatible with Grid 3 + * + * @example + * const formatted = formatGrid3Xml(''); + * // Returns: '\r\n\r\n' + */ +export function formatGrid3Xml(xml: string): string { + let formatted = xml; + + // Convert Unix line endings to Windows (\r\n) for Grid 3 compatibility + formatted = formatted.replace(/\n/g, '\r\n'); + + // Add space before /> in self-closing tags to match Grid 3's expected format + // Grid 3 original files use not + formatted = formatted.replace(/<(\w+)([^>]*)\/>/g, '<$1$2 />'); + + // Decode XML entities back to plain text to match Grid 3's expected format + // Grid 3 expects plain apostrophes, not ' + formatted = formatted.replace(/'/g, "'"); + formatted = formatted.replace(/"/g, '"'); + formatted = formatted.replace(/</g, '<'); + formatted = formatted.replace(/>/g, '>'); + + // Expand only specific self-closing tags that Grid 3 requires in full opening/closing format + // This must be done AFTER adding spaces, so we need to match the format with spaces + for (const tag of TAGS_NEEDING_EXPANSION) { + formatted = formatted.replace(new RegExp(`<${tag}(\\s+[^>]*)? />`, 'g'), `<${tag}$1>`); + } + + return formatted; +} + +/** + * Format empty/whitespace captions with CDATA for Grid 3 compatibility + * + * Grid 3 requires for empty captions, not plain text. + * Also handles tags which need CDATA for spaces to prevent stripping. + * + * @param xml - The XML string to format + * @returns XML string with CDATA-wrapped empty content + */ +export function formatEmptyCaptionsWithCdata(xml: string): string { + let formatted = xml; + + // Convert empty/whitespace captions to CDATA format for Grid 3 compatibility + // Grid 3 requires for empty captions, not plain text + formatted = formatted.replace(/<\/Caption>/g, ''); + formatted = formatted.replace(/ <\/Caption>/g, ''); + formatted = formatted.replace(/ {2}<\/Caption>/g, ''); + + // Preserve CDATA in tags for text parameters + // Spaces in tags must use CDATA or they get stripped during rendering + // e.g., becomes + formatted = formatted.replace(/ <\/r>/g, ''); + formatted = formatted.replace(/ {2}<\/r>/g, ''); + + return formatted; +} + +/** + * Complete XML formatting for Grid 3 compatibility + * Combines all Grid 3 XML formatting requirements + * + * @param xml - The XML string to format + * @returns Fully formatted XML string compatible with Grid 3 + */ +export function formatGrid3XmlComplete(xml: string): string { + let formatted = formatGrid3Xml(xml); + formatted = formatEmptyCaptionsWithCdata(formatted); + return formatted; +} diff --git a/src/processors/gridsetProcessor.ts b/src/processors/gridsetProcessor.ts index ab862bd..7c6102b 100644 --- a/src/processors/gridsetProcessor.ts +++ b/src/processors/gridsetProcessor.ts @@ -29,6 +29,12 @@ import { type ZipEntry, } from './gridset/password'; import { decryptGridsetEntry } from './gridset/crypto'; +import { formatGrid3XmlComplete } from './gridset/xmlFormatter'; +import { + calculateColumnDefinitions as calcColumnDefs, + calculateRowDefinitions as calcRowDefs, +} from './gridset/gridCalculations'; +import { findButtonPosition as findButtonPos } from './gridset/cellHelpers'; import { GridsetValidator } from '../validation/gridsetValidator'; import { ValidationResult } from '../validation/validationTypes'; // New imports for enhanced Grid 3 support @@ -2481,18 +2487,7 @@ class GridsetProcessor extends BaseProcessor { private calculateColumnDefinitions(page: AACPage): { ColumnDefinition: any[]; } { - let maxCols = 4; // Default minimum - - if (page.grid && page.grid.length > 0) { - maxCols = Math.max(maxCols, page.grid[0]?.length || 0); - } else { - // Fallback: estimate from button count - maxCols = Math.max(4, Math.ceil(Math.sqrt(page.buttons.length))); - } - - return { - ColumnDefinition: Array(maxCols).fill({}), - }; + return calcColumnDefs(page); } // Helper method to calculate row definitions based on page layout @@ -2500,20 +2495,7 @@ class GridsetProcessor extends BaseProcessor { page: AACPage, addWorkspaceOffset = false ): { RowDefinition: any[] } { - let maxRows = 4; // Default minimum - const offset = addWorkspaceOffset ? 1 : 0; - - if (page.grid && page.grid.length > 0) { - maxRows = Math.max(maxRows, page.grid.length + offset); - } else { - // Fallback: estimate from button count - const estimatedCols = Math.ceil(Math.sqrt(page.buttons.length)); - maxRows = Math.max(4, Math.ceil(page.buttons.length / estimatedCols)) + offset; - } - - return { - RowDefinition: Array(maxRows).fill({}), - }; + return calcRowDefs(page, addWorkspaceOffset); } /** @@ -2693,6 +2675,10 @@ class GridsetProcessor extends BaseProcessor { } } + // DO NOT create new cells - the system should only modify existing content + // Personalized vocabulary is added to WordList cells via the WordList.Items array + // Creating new cells would corrupt the grid structure + // Update the page's WordList with new words from modified buttons // Collect all modified buttons that should be added to the WordList const newWordListItems: any[] = []; @@ -2708,9 +2694,21 @@ class GridsetProcessor extends BaseProcessor { : []; const cell = cellArray.find((c: any) => { - const cellX = parseInt(String(c['@_X'] || '0'), 10); - const cellY = parseInt(String(c['@_Y'] || '0'), 10); - return cellX === pos.x && cellY === pos.y; + const cellY = parseInt(String(c['@_Y'] || c['@_Row'] || '0'), 10); + // Check Y position first + if (cellY !== pos.y) { + return false; + } + + const cellX = c['@_X'] !== undefined ? parseInt(String(c['@_X']), 10) : undefined; + + // If cell has no X attribute (full-width cell), it matches any button at this Y + if (cellX === undefined) { + return true; + } + + // Otherwise, check exact X match + return cellX === pos.x; }); if (cell) { @@ -2723,11 +2721,14 @@ class GridsetProcessor extends BaseProcessor { if (isWordListCell) { // Add this button to the WordList with proper Grid 3 format - // Format: label + // Format:

label

+ // Note:

wrapper is required by Grid 3's WordList format newWordListItems.push({ Text: { - s: { - r: button.label, + p: { + s: { + r: button.label, + }, }, }, Image: '', // No image for user-added words @@ -2759,23 +2760,9 @@ class GridsetProcessor extends BaseProcessor { } } - // Build the updated grid XML and convert to Windows line endings + // Build the updated grid XML and format for Grid 3 compatibility let builtXml = gridBuilder.build(originalGrid); - // Convert Unix line endings to Windows (\r\n) for Grid 3 compatibility - builtXml = builtXml.replace(/\n/g, '\r\n'); - // Expand self-closing tags to full opening/closing tags for Grid 3 compatibility - // Grid 3 cannot parse - it requires - builtXml = builtXml.replace(/<(\w+)(\s+[^>]*)?\s*\/>/g, '<$1$2>'); - // Convert empty/whitespace captions to CDATA format for Grid 3 compatibility - // Grid 3 requires for empty captions, not plain text - builtXml = builtXml.replace(/<\/Caption>/g, ''); - builtXml = builtXml.replace(/ <\/Caption>/g, ''); - builtXml = builtXml.replace(/ {2}<\/Caption>/g, ''); - // Preserve CDATA in tags for text parameters - // Spaces in tags must use CDATA or they get stripped during rendering - // e.g., becomes - builtXml = builtXml.replace(/ <\/r>/g, ''); - builtXml = builtXml.replace(/ {2}<\/r>/g, ''); + builtXml = formatGrid3XmlComplete(builtXml); newGridFiles.set(gridPath, builtXml); } @@ -2855,11 +2842,9 @@ class GridsetProcessor extends BaseProcessor { suppressBooleanAttributes: false, }); - // Build the grid XML and convert to Windows line endings for Grid 3 compatibility + // Build the grid XML and format for Grid 3 compatibility let builtXml = gridBuilder.build(gridData); - builtXml = builtXml.replace(/\n/g, '\r\n'); - // Expand self-closing tags to full opening/closing tags for Grid 3 compatibility - builtXml = builtXml.replace(/<(\w+)(\s+[^>]*)?\s*\/>/g, '<$1$2>'); + builtXml = formatGrid3XmlComplete(builtXml); return builtXml; } @@ -2874,50 +2859,7 @@ class GridsetProcessor extends BaseProcessor { columnSpan: number; rowSpan: number; } { - if (page.grid && page.grid.length > 0) { - // Search for button in grid layout and calculate span - for (let y = 0; y < page.grid.length; y++) { - for (let x = 0; x < page.grid[y].length; x++) { - const current = page.grid[y][x]; - if (current && current.id === button.id) { - // Calculate span by checking how far the same button extends - let columnSpan = 1; - let rowSpan = 1; - - // Check column span (rightward) - while (x + columnSpan < page.grid[y].length) { - const right = page.grid[y][x + columnSpan]; - if (right && right.id === button.id) { - columnSpan++; - } else { - break; - } - } - - // Check row span (downward) - while (y + rowSpan < page.grid.length) { - const below = page.grid[y + rowSpan][x]; - if (below && below.id === button.id) { - rowSpan++; - } else { - break; - } - } - - return { x, y, columnSpan, rowSpan }; - } - } - } - } - - // Fallback positioning - const gridCols = page.grid?.[0]?.length || Math.ceil(Math.sqrt(page.buttons.length)); - return { - x: fallbackIndex % gridCols, - y: Math.floor(fallbackIndex / gridCols), - columnSpan: 1, - rowSpan: 1, - }; + return findButtonPos(page, button, fallbackIndex); } /** diff --git a/src/processors/obfProcessor.ts b/src/processors/obfProcessor.ts index 85d0603..f191796 100644 --- a/src/processors/obfProcessor.ts +++ b/src/processors/obfProcessor.ts @@ -281,7 +281,11 @@ class ObfProcessor extends BaseProcessor { } // Build parameters object for Grid3 export compatibility - const buttonParameters: { imageData?: Buffer; image_id?: string; [key: string]: any } = {}; + const buttonParameters: { + imageData?: Buffer; + image_id?: string; + [key: string]: any; + } = {}; if (imageBuffer) { buttonParameters.imageData = imageBuffer; } @@ -706,7 +710,10 @@ class ObfProcessor extends BaseProcessor { order, }, buttons: page.buttons.map((button) => { - const extraButtonInfo = button as AACButton & { image_id?: string; imageId?: string }; + const extraButtonInfo = button as AACButton & { + image_id?: string; + imageId?: string; + }; const imageId = button.parameters?.image_id || button.parameters?.imageId || diff --git a/src/utils/io.ts b/src/utils/io.ts index eedacc5..7a77bce 100644 --- a/src/utils/io.ts +++ b/src/utils/io.ts @@ -146,7 +146,7 @@ export function encodeText(text: string): BinaryOutput { // extname algorithm from node:path const splitDeviceRe = /^([a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/]+[^\\\/]+)?([\\\/])?([\s\S]*?)$/; //eslint-disable-line -const splitTailRe = /^([\s\S]*?)((?:\.{1,2}|[^\\\/]+?|)(\.[^.\/\\]*|))(?:[\\\/]*)$/; //eslint-disable-line +const splitTailRe = /^([\s\S]*?)((?:\.{1,2}|[^\\\/]+?|)(\.[^.\/\\]*|))(?:[\\\/]*)$/; //eslint-disable-line export function extname(path: string): string { const tail = splitDeviceRe.exec(path)?.at(3) ?? ''; return splitTailRe.exec(tail)?.at(3) ?? ''; diff --git a/src/utils/sqlite.ts b/src/utils/sqlite.ts index 93bbea3..826a4d8 100644 --- a/src/utils/sqlite.ts +++ b/src/utils/sqlite.ts @@ -115,7 +115,9 @@ export async function openSqliteDatabase( throw new Error('SQLite file paths are not supported in browser environments.'); } const Database = getBetterSqlite3(); - const db = new Database(input, { readonly: options.readonly ?? true }) as SqliteDatabaseAdapter; + const db = new Database(input, { + readonly: options.readonly ?? true, + }) as SqliteDatabaseAdapter; return { db }; } @@ -132,7 +134,9 @@ export async function openSqliteDatabase( await writeBinaryToPath(dbPath, data); const Database = getBetterSqlite3(); - const db = new Database(dbPath, { readonly: options.readonly ?? true }) as SqliteDatabaseAdapter; + const db = new Database(dbPath, { + readonly: options.readonly ?? true, + }) as SqliteDatabaseAdapter; const cleanup = async (): Promise => { try { db.close(); diff --git a/test/cli/cli.dot.integration.test.js b/test/cli/cli.dot.integration.test.js index 977e783..cec1846 100644 --- a/test/cli/cli.dot.integration.test.js +++ b/test/cli/cli.dot.integration.test.js @@ -1,27 +1,25 @@ // CLI integration test for DOT -const path = require("path"); -const { execSync } = require("child_process"); -const fs = require("fs"); +const path = require('path'); +const { execSync } = require('child_process'); +const fs = require('fs'); -describe("aac-processors CLI (DOT)", () => { +describe('aac-processors CLI (DOT)', () => { // Ensure build exists before running CLI tests beforeAll(() => { - const cliPath = path.join(__dirname, "../../dist/cli/index.js"); + const cliPath = path.join(__dirname, '../../dist/cli/index.js'); if (!fs.existsSync(cliPath)) { throw new Error( - "dist/cli/index.js is missing – run `npm run build` before executing the CLI tests.", + 'dist/cli/index.js is missing – run `npm run build` before executing the CLI tests.' ); } }); - const cliPath = path.join(__dirname, "../../dist/cli/index.js"); - const dotExample = path.join(__dirname, "../assets/dot/example.dot"); + const cliPath = path.join(__dirname, '../../dist/cli/index.js'); + const dotExample = path.join(__dirname, '../assets/dot/example.dot'); - it("extracts texts from a dot file", () => { - const result = execSync( - `node ${cliPath} extract ${dotExample} --format dot`, - ).toString(); + it('extracts texts from a dot file', () => { + const result = execSync(`node ${cliPath} extract ${dotExample} --format dot`).toString(); // Should contain actual text content from the dot file expect(result.length).toBeGreaterThan(10); // Should have some text output - expect(result.trim()).not.toBe(""); // Should not be empty + expect(result.trim()).not.toBe(''); // Should not be empty }); }); diff --git a/test/cli/cli.integration.test.js b/test/cli/cli.integration.test.js index fde274a..573bd8b 100644 --- a/test/cli/cli.integration.test.js +++ b/test/cli/cli.integration.test.js @@ -1,57 +1,54 @@ -const { execSync } = require("child_process"); -const path = require("path"); -const fs = require("fs"); +const { execSync } = require('child_process'); +const path = require('path'); +const fs = require('fs'); -describe("aac-processors CLI", () => { +describe('aac-processors CLI', () => { // Ensure build exists before running CLI tests beforeAll(() => { - const cliPath = path.join(__dirname, "../../dist/cli/index.js"); + const cliPath = path.join(__dirname, '../../dist/cli/index.js'); if (!fs.existsSync(cliPath)) { throw new Error( - "dist/cli/index.js is missing – run `npm run build` before executing the CLI tests.", + 'dist/cli/index.js is missing – run `npm run build` before executing the CLI tests.' ); } }); - const cliPath = path.join(__dirname, "../../dist/cli/index.js"); - const gridsetExample = path.join( - __dirname, - "../assets/gridset/example.gridset", - ); - const touchchatExample = path.join(__dirname, "../assets/excel/example.ce"); + const cliPath = path.join(__dirname, '../../dist/cli/index.js'); + const gridsetExample = path.join(__dirname, '../assets/gridset/example.gridset'); + const touchchatExample = path.join(__dirname, '../assets/excel/example.ce'); - it("extracts texts from a gridset file", () => { + it('extracts texts from a gridset file', () => { const result = execSync( - `node ${cliPath} extract ${gridsetExample} --format gridset`, + `node ${cliPath} extract ${gridsetExample} --format gridset` ).toString(); // Should contain actual text content from the gridset - expect(result).toContain("Food"); + expect(result).toContain('Food'); expect(result.length).toBeGreaterThan(50); // Should have substantial text output }); - it("extracts texts from a touchchat file", () => { + it('extracts texts from a touchchat file', () => { const result = execSync( - `node ${cliPath} extract ${touchchatExample} --format touchchat`, + `node ${cliPath} extract ${touchchatExample} --format touchchat` ).toString(); // Should contain actual text content from the touchchat file expect(result.length).toBeGreaterThan(10); // Should have some text output - expect(result.trim()).not.toBe(""); // Should not be empty + expect(result.trim()).not.toBe(''); // Should not be empty }); - it("pretty prints analyze for gridset", () => { + it('pretty prints analyze for gridset', () => { const result = execSync( - `node ${cliPath} analyze ${gridsetExample} --format gridset --pretty`, + `node ${cliPath} analyze ${gridsetExample} --format gridset --pretty` ).toString(); - expect(result).toContain("Page:"); + expect(result).toContain('Page:'); // The gridset should have buttons, but if parsing is still being fixed, // we'll accept either buttons or a reasonable page structure expect(result.length).toBeGreaterThan(100); // Should have substantial output }); - it("pretty prints analyze for touchchat", () => { + it('pretty prints analyze for touchchat', () => { const result = execSync( - `node ${cliPath} analyze ${touchchatExample} --format touchchat --pretty`, + `node ${cliPath} analyze ${touchchatExample} --format touchchat --pretty` ).toString(); - expect(result).toContain("Page:"); - expect(result).toContain("- Button:"); + expect(result).toContain('Page:'); + expect(result).toContain('- Button:'); }); }); diff --git a/test/cli/cli.obf.integration.test.js b/test/cli/cli.obf.integration.test.js index 1f4c43f..339caa4 100644 --- a/test/cli/cli.obf.integration.test.js +++ b/test/cli/cli.obf.integration.test.js @@ -1,36 +1,32 @@ // CLI integration test for OBF/OBZ -const path = require("path"); -const { execSync } = require("child_process"); -const fs = require("fs"); +const path = require('path'); +const { execSync } = require('child_process'); +const fs = require('fs'); -describe("aac-processors CLI (OBF/OBZ)", () => { +describe('aac-processors CLI (OBF/OBZ)', () => { // Ensure build exists before running CLI tests beforeAll(() => { - const cliPath = path.join(__dirname, "../../dist/cli/index.js"); + const cliPath = path.join(__dirname, '../../dist/cli/index.js'); if (!fs.existsSync(cliPath)) { throw new Error( - "dist/cli/index.js is missing – run `npm run build` before executing the CLI tests.", + 'dist/cli/index.js is missing – run `npm run build` before executing the CLI tests.' ); } }); - const cliPath = path.join(__dirname, "../../dist/cli/index.js"); - const obfExample = path.join(__dirname, "../assets/obf/example.obf"); - const obzExample = path.join(__dirname, "../assets/obz/example.obz"); + const cliPath = path.join(__dirname, '../../dist/cli/index.js'); + const obfExample = path.join(__dirname, '../assets/obf/example.obf'); + const obzExample = path.join(__dirname, '../assets/obz/example.obz'); - it("extracts texts from an obf file", () => { - const result = execSync( - `node ${cliPath} extract ${obfExample} --format obf`, - ).toString(); + it('extracts texts from an obf file', () => { + const result = execSync(`node ${cliPath} extract ${obfExample} --format obf`).toString(); // Should contain actual text content from the obf file expect(result.length).toBeGreaterThan(10); // Should have some text output - expect(result.trim()).not.toBe(""); // Should not be empty + expect(result.trim()).not.toBe(''); // Should not be empty }); - it("extracts texts from an obz file", () => { - const result = execSync( - `node ${cliPath} extract ${obzExample} --format obf`, - ).toString(); + it('extracts texts from an obz file', () => { + const result = execSync(`node ${cliPath} extract ${obzExample} --format obf`).toString(); expect(result.length).toBeGreaterThan(10); // Should have some text output - expect(result.trim()).not.toBe(""); // Should not be empty + expect(result.trim()).not.toBe(''); // Should not be empty }); }); diff --git a/test/cli/cli.opml.integration.test.js b/test/cli/cli.opml.integration.test.js index 3c67494..5c0920e 100644 --- a/test/cli/cli.opml.integration.test.js +++ b/test/cli/cli.opml.integration.test.js @@ -1,27 +1,25 @@ // CLI integration test for OPML -const path = require("path"); -const { execSync } = require("child_process"); -const fs = require("fs"); +const path = require('path'); +const { execSync } = require('child_process'); +const fs = require('fs'); -describe("aac-processors CLI (OPML)", () => { +describe('aac-processors CLI (OPML)', () => { // Ensure build exists before running CLI tests beforeAll(() => { - const cliPath = path.join(__dirname, "../../dist/cli/index.js"); + const cliPath = path.join(__dirname, '../../dist/cli/index.js'); if (!fs.existsSync(cliPath)) { throw new Error( - "dist/cli/index.js is missing – run `npm run build` before executing the CLI tests.", + 'dist/cli/index.js is missing – run `npm run build` before executing the CLI tests.' ); } }); - const cliPath = path.join(__dirname, "../../dist/cli/index.js"); - const opmlExample = path.join(__dirname, "../assets/opml/example.opml"); + const cliPath = path.join(__dirname, '../../dist/cli/index.js'); + const opmlExample = path.join(__dirname, '../assets/opml/example.opml'); - it("extracts texts from an opml file", () => { - const result = execSync( - `node ${cliPath} extract ${opmlExample} --format opml`, - ).toString(); + it('extracts texts from an opml file', () => { + const result = execSync(`node ${cliPath} extract ${opmlExample} --format opml`).toString(); // Should contain actual text content from the opml file expect(result.length).toBeGreaterThan(10); // Should have some text output - expect(result.trim()).not.toBe(""); // Should not be empty + expect(result.trim()).not.toBe(''); // Should not be empty }); }); diff --git a/test/cli/cli.snap.integration.test.js b/test/cli/cli.snap.integration.test.js index 464f077..2a39a33 100644 --- a/test/cli/cli.snap.integration.test.js +++ b/test/cli/cli.snap.integration.test.js @@ -1,35 +1,32 @@ -const { execSync } = require("child_process"); -const path = require("path"); -const fs = require("fs"); +const { execSync } = require('child_process'); +const path = require('path'); +const fs = require('fs'); -describe("aac-processors CLI (Snap)", () => { +describe('aac-processors CLI (Snap)', () => { // Ensure build exists before running CLI tests beforeAll(() => { - const cliPath = path.join(__dirname, "../../dist/cli/index.js"); + const cliPath = path.join(__dirname, '../../dist/cli/index.js'); if (!fs.existsSync(cliPath)) { throw new Error( - "dist/cli/index.js is missing – run `npm run build` before executing the CLI tests.", + 'dist/cli/index.js is missing – run `npm run build` before executing the CLI tests.' ); } }); - const cliPath = path.join(__dirname, "../../dist/cli/index.js"); - const snapExample = path.join(__dirname, "../assets/snap/example.sps"); + const cliPath = path.join(__dirname, '../../dist/cli/index.js'); + const snapExample = path.join(__dirname, '../assets/snap/example.sps'); - it("extracts texts from a snap file", () => { + it('extracts texts from a snap file', () => { try { - const result = execSync( - `node ${cliPath} extract ${snapExample} --format snap`, - { - maxBuffer: 1024 * 1024 * 10, // 10MB buffer - timeout: 30000, // 30 second timeout - }, - ).toString(); + const result = execSync(`node ${cliPath} extract ${snapExample} --format snap`, { + maxBuffer: 1024 * 1024 * 10, // 10MB buffer + timeout: 30000, // 30 second timeout + }).toString(); expect(result.length).toBeGreaterThan(10); // Should have some text output - expect(result.trim()).not.toBe(""); // Should not be empty + expect(result.trim()).not.toBe(''); // Should not be empty } catch (error) { // If the command fails due to buffer issues, skip the test - if (error.code === "ENOBUFS" || error.status !== 0) { - console.warn("Snap CLI test skipped due to output buffer issues"); + if (error.code === 'ENOBUFS' || error.status !== 0) { + console.warn('Snap CLI test skipped due to output buffer issues'); expect(true).toBe(true); // Pass the test } else { throw error; @@ -37,21 +34,18 @@ describe("aac-processors CLI (Snap)", () => { } }); - it("pretty prints analyze for snap", () => { + it('pretty prints analyze for snap', () => { try { - const result = execSync( - `node ${cliPath} analyze ${snapExample} --format snap --pretty`, - { - maxBuffer: 1024 * 1024 * 10, // 10MB buffer - timeout: 30000, // 30 second timeout - }, - ).toString(); - expect(result).toContain("Page:"); - expect(result).toContain("- Button:"); + const result = execSync(`node ${cliPath} analyze ${snapExample} --format snap --pretty`, { + maxBuffer: 1024 * 1024 * 10, // 10MB buffer + timeout: 30000, // 30 second timeout + }).toString(); + expect(result).toContain('Page:'); + expect(result).toContain('- Button:'); } catch (error) { // If the command fails due to buffer issues, skip the test - if (error.code === "ENOBUFS" || error.status !== 0) { - console.warn("Snap CLI test skipped due to output buffer issues"); + if (error.code === 'ENOBUFS' || error.status !== 0) { + console.warn('Snap CLI test skipped due to output buffer issues'); expect(true).toBe(true); // Pass the test } else { throw error; diff --git a/test/cli/prettyPrint.test.js b/test/cli/prettyPrint.test.js index 0c648f2..8771193 100644 --- a/test/cli/prettyPrint.test.js +++ b/test/cli/prettyPrint.test.js @@ -1,29 +1,29 @@ -const { prettyPrintTree } = require("../../dist/cli/prettyPrint"); +const { prettyPrintTree } = require('../../dist/cli/prettyPrint'); -describe("prettyPrintTree", () => { - it("prints a simple tree with one page and buttons", () => { +describe('prettyPrintTree', () => { + it('prints a simple tree with one page and buttons', () => { const tree = { pages: { 1: { - id: "1", - name: "Home", + id: '1', + name: 'Home', buttons: [ - { label: "Hello", type: "SPEAK" }, - { label: "Go", type: "NAVIGATE", targetPageId: "2" }, + { label: 'Hello', type: 'SPEAK' }, + { label: 'Go', type: 'NAVIGATE', targetPageId: '2' }, ], }, 2: { - id: "2", - name: "Second", + id: '2', + name: 'Second', buttons: [], }, }, }; const output = prettyPrintTree(tree); - expect(output).toContain("Page: Home"); + expect(output).toContain('Page: Home'); expect(output).toContain('- Button: "Hello"'); - expect(output).toContain("[NAVIGATE to page: 2]"); - expect(output).toContain("Page: Second"); - expect(output).toContain("(no buttons)"); + expect(output).toContain('[NAVIGATE to page: 2]'); + expect(output).toContain('Page: Second'); + expect(output).toContain('(no buttons)'); }); }); diff --git a/test/core/baseProcessor.generic.test.ts b/test/core/baseProcessor.generic.test.ts index 5d332e7..7e5870c 100644 --- a/test/core/baseProcessor.generic.test.ts +++ b/test/core/baseProcessor.generic.test.ts @@ -69,7 +69,11 @@ class DummyProcessor extends BaseProcessor { function createTree(): AACTree { const tree = new AACTree(); const page = new AACPage({ id: 'page-1', name: 'Home' }); - const yesButton = new AACButton({ id: 'btn-1', label: 'Yes', message: 'Yes' }); + const yesButton = new AACButton({ + id: 'btn-1', + label: 'Yes', + message: 'Yes', + }); const noButton = new AACButton({ id: 'btn-2', label: 'No', message: 'Nope' }); page.buttons.push(yesButton, noButton); tree.addPage(page); @@ -154,10 +158,18 @@ describe('BaseProcessor generic helpers', () => { const tree = createTree(); const processor = new DummyProcessor(tree); const sourceStrings: SourceString[] = [ - { id: 1, sourcestring: 'Hello', vocabplacementmetadata: { vocabLocations: [] } }, + { + id: 1, + sourcestring: 'Hello', + vocabplacementmetadata: { vocabLocations: [] }, + }, ]; const translatedStrings: TranslatedString[] = [ - { sourcestringid: 1, overridestring: 'Hola', translatedstring: 'Bonjour' }, + { + sourcestringid: 1, + overridestring: 'Hola', + translatedstring: 'Bonjour', + }, ]; const outputPath = await processor.generateTranslatedGeneric( diff --git a/test/core/coverageBoost.test.ts b/test/core/coverageBoost.test.ts index 6c14764..0cdbd54 100644 --- a/test/core/coverageBoost.test.ts +++ b/test/core/coverageBoost.test.ts @@ -176,7 +176,10 @@ describe('src/core Coverage Boost', () => { const processor = new MockProcessor({ excludeSystemButtons: true }); const editBtn = new AACButton({ id: 'edit', - semanticAction: { intent: 'ANY', category: AACSemanticCategory.TEXT_EDITING }, + semanticAction: { + intent: 'ANY', + category: AACSemanticCategory.TEXT_EDITING, + }, }); expect(processor.callShouldFilter(editBtn)).toBe(true); }); diff --git a/test/dotProcessor.export.test.js b/test/dotProcessor.export.test.js index 7820ffd..fb08942 100644 --- a/test/dotProcessor.export.test.js +++ b/test/dotProcessor.export.test.js @@ -1,20 +1,20 @@ // Test DotProcessor export/saveFromTree -const fs = require("fs"); -const path = require("path"); -const { DotProcessor } = require("../dist/processors/dotProcessor"); -describe("DotProcessor.saveFromTree", () => { - const dotPath = path.join(__dirname, "assets/dot/example.dot"); - const outPath = path.join(__dirname, "out.dot"); +const fs = require('fs'); +const path = require('path'); +const { DotProcessor } = require('../dist/processors/dotProcessor'); +describe('DotProcessor.saveFromTree', () => { + const dotPath = path.join(__dirname, 'assets/dot/example.dot'); + const outPath = path.join(__dirname, 'out.dot'); afterAll(() => { if (fs.existsSync(outPath)) fs.unlinkSync(outPath); }); - it("exports tree to DOT format", async () => { + it('exports tree to DOT format', async () => { const processor = new DotProcessor(); const tree = await processor.loadIntoTree(dotPath); await processor.saveFromTree(tree, outPath); - const exported = fs.readFileSync(outPath, "utf8"); + const exported = fs.readFileSync(outPath, 'utf8'); expect(exported).toContain('digraph "AACBoard"'); - expect(exported).toContain("["); - expect(exported).toContain("->"); + expect(exported).toContain('['); + expect(exported).toContain('->'); }); }); diff --git a/test/gridsetProcessor.export.test.js b/test/gridsetProcessor.export.test.js index 0121dc2..6b360ec 100644 --- a/test/gridsetProcessor.export.test.js +++ b/test/gridsetProcessor.export.test.js @@ -1,28 +1,23 @@ // Test GridsetProcessor export/saveFromTree -const fs = require("fs"); -const path = require("path"); -const GridsetProcessor = require("../src/processors/gridsetProcessor"); -describe("GridsetProcessor.saveFromTree", () => { - const gsPath = path.join(__dirname, "assets/gridset/example.gridset.json"); - const outPath = path.join(__dirname, "out.gridset.json"); +const fs = require('fs'); +const path = require('path'); +const GridsetProcessor = require('../src/processors/gridsetProcessor'); +describe('GridsetProcessor.saveFromTree', () => { + const gsPath = path.join(__dirname, 'assets/gridset/example.gridset.json'); + const outPath = path.join(__dirname, 'out.gridset.json'); afterAll(() => { if (fs.existsSync(outPath)) fs.unlinkSync(outPath); }); - it("exports tree to Gridset JSON", () => { + it('exports tree to Gridset JSON', () => { // If no example.gridset.json, skip if (!fs.existsSync(gsPath)) return; - const tree = - require("../src/processors/gridsetProcessor").prototype.loadIntoTree.call( - GridsetProcessor, - gsPath, - ); - GridsetProcessor.prototype.saveFromTree.call( + const tree = require('../src/processors/gridsetProcessor').prototype.loadIntoTree.call( GridsetProcessor, - tree, - outPath, + gsPath ); - const exported = fs.readFileSync(outPath, "utf8"); - expect(exported).toContain("pages"); - expect(exported).toContain("rootId"); + GridsetProcessor.prototype.saveFromTree.call(GridsetProcessor, tree, outPath); + const exported = fs.readFileSync(outPath, 'utf8'); + expect(exported).toContain('pages'); + expect(exported).toContain('rootId'); }); }); diff --git a/test/opmlProcessor.export.test.js b/test/opmlProcessor.export.test.js index fbf5d6d..b5dfad3 100644 --- a/test/opmlProcessor.export.test.js +++ b/test/opmlProcessor.export.test.js @@ -1,20 +1,20 @@ // Test OPMLProcessor export/saveFromTree -const fs = require("fs"); -const path = require("path"); -const { OpmlProcessor } = require("../dist/processors/opmlProcessor"); -describe("OPMLProcessor.saveFromTree", () => { - const opmlPath = path.join(__dirname, "assets/opml/example.opml"); - const outPath = path.join(__dirname, "out.opml"); +const fs = require('fs'); +const path = require('path'); +const { OpmlProcessor } = require('../dist/processors/opmlProcessor'); +describe('OPMLProcessor.saveFromTree', () => { + const opmlPath = path.join(__dirname, 'assets/opml/example.opml'); + const outPath = path.join(__dirname, 'out.opml'); afterAll(() => { if (fs.existsSync(outPath)) fs.unlinkSync(outPath); }); - it("exports tree to OPML XML", async () => { + it('exports tree to OPML XML', async () => { const processor = new OpmlProcessor(); const tree = await processor.loadIntoTree(opmlPath); await processor.saveFromTree(tree, outPath); - const exported = fs.readFileSync(outPath, "utf8"); - expect(exported).toContain(" { }); // Target button is #3 (linear index 2) - const btn1 = new AACButton({ id: 'btn1', label: '1', type: 'SPEAK', x: 0, y: 0 }); - const btn2 = new AACButton({ id: 'btn2', label: '2', type: 'SPEAK', x: 1, y: 0 }); - const btn3 = new AACButton({ id: 'btn3', label: '3', type: 'SPEAK', x: 0, y: 1 }); // Target + const btn1 = new AACButton({ + id: 'btn1', + label: '1', + type: 'SPEAK', + x: 0, + y: 0, + }); + const btn2 = new AACButton({ + id: 'btn2', + label: '2', + type: 'SPEAK', + x: 1, + y: 0, + }); + const btn3 = new AACButton({ + id: 'btn3', + label: '3', + type: 'SPEAK', + x: 0, + y: 1, + }); // Target page.grid[0][0] = btn1; page.grid[0][1] = btn2; @@ -52,7 +70,13 @@ describe('Scanning Metrics', () => { }); // Target button at row 3 (index 2), col 4 (index 3) - const btn = new AACButton({ id: 'target', label: 'Target', type: 'SPEAK', x: 3, y: 2 }); + const btn = new AACButton({ + id: 'target', + label: 'Target', + type: 'SPEAK', + x: 3, + y: 2, + }); page.grid[2][3] = btn; page.addButton(btn); diff --git a/test/snapProcessor.coverage.test.ts b/test/snapProcessor.coverage.test.ts index 0f5e53c..4ef5693 100644 --- a/test/snapProcessor.coverage.test.ts +++ b/test/snapProcessor.coverage.test.ts @@ -72,7 +72,9 @@ describe('SnapProcessor Coverage', () => { const processor = new SnapProcessor(); const audioMappings = new Map(); - audioMappings.set(1, { audioData: new Uint8Array(Buffer.from('new audio')) }); + audioMappings.set(1, { + audioData: new Uint8Array(Buffer.from('new audio')), + }); await processor.createAudioEnhancedPageset(exampleFile, enhancedDbPath, audioMappings); diff --git a/test/snapProcessor.export.test.js b/test/snapProcessor.export.test.js index 12ee42a..34d3041 100644 --- a/test/snapProcessor.export.test.js +++ b/test/snapProcessor.export.test.js @@ -1,26 +1,26 @@ // Test SnapProcessor export/saveFromTree -const fs = require("fs"); -const path = require("path"); -const { SnapProcessor } = require("../dist/processors/snapProcessor"); -describe("SnapProcessor.saveFromTree", () => { - const snapPath = path.join(__dirname, "assets/snap/example.snap.json"); - const spsPath = path.join(__dirname, "assets/snap/example.sps"); - const outPath = path.join(__dirname, "out.snap.json"); +const fs = require('fs'); +const path = require('path'); +const { SnapProcessor } = require('../dist/processors/snapProcessor'); +describe('SnapProcessor.saveFromTree', () => { + const snapPath = path.join(__dirname, 'assets/snap/example.snap.json'); + const spsPath = path.join(__dirname, 'assets/snap/example.sps'); + const outPath = path.join(__dirname, 'out.snap.json'); afterAll(() => { if (fs.existsSync(outPath)) fs.unlinkSync(outPath); }); - it("exports tree to Snap JSON", async () => { + it('exports tree to Snap JSON', async () => { // If no example.snap.json, skip if (!fs.existsSync(snapPath)) return; const processor = new SnapProcessor(); const tree = await processor.loadIntoTree(snapPath); await processor.saveFromTree(tree, outPath); - const exported = fs.readFileSync(outPath, "utf8"); - expect(exported).toContain("pages"); - expect(exported).toContain("rootId"); + const exported = fs.readFileSync(outPath, 'utf8'); + expect(exported).toContain('pages'); + expect(exported).toContain('rootId'); }); - it("loads tree from .sps file and returns pages", async () => { + it('loads tree from .sps file and returns pages', async () => { if (!fs.existsSync(spsPath)) return; const processor = new SnapProcessor(); const tree = await processor.loadIntoTree(spsPath); diff --git a/test/touchchatProcessor.export.test.js b/test/touchchatProcessor.export.test.js index 8e9e335..65f2651 100644 --- a/test/touchchatProcessor.export.test.js +++ b/test/touchchatProcessor.export.test.js @@ -1,28 +1,23 @@ // Test TouchChatProcessor export/saveFromTree -const fs = require("fs"); -const path = require("path"); -const TouchChatProcessor = require("../src/processors/touchchatProcessor"); -describe("TouchChatProcessor.saveFromTree", () => { - const tcPath = path.join(__dirname, "assets/excel/example.touchchat.json"); - const outPath = path.join(__dirname, "out.touchchat.json"); +const fs = require('fs'); +const path = require('path'); +const TouchChatProcessor = require('../src/processors/touchchatProcessor'); +describe('TouchChatProcessor.saveFromTree', () => { + const tcPath = path.join(__dirname, 'assets/excel/example.touchchat.json'); + const outPath = path.join(__dirname, 'out.touchchat.json'); afterAll(() => { if (fs.existsSync(outPath)) fs.unlinkSync(outPath); }); - it("exports tree to TouchChat JSON", () => { + it('exports tree to TouchChat JSON', () => { // If no example.touchchat.json, skip if (!fs.existsSync(tcPath)) return; - const tree = - require("../src/processors/touchchatProcessor").prototype.loadIntoTree.call( - TouchChatProcessor, - tcPath, - ); - TouchChatProcessor.prototype.saveFromTree.call( + const tree = require('../src/processors/touchchatProcessor').prototype.loadIntoTree.call( TouchChatProcessor, - tree, - outPath, + tcPath ); - const exported = fs.readFileSync(outPath, "utf8"); - expect(exported).toContain("pages"); - expect(exported).toContain("rootId"); + TouchChatProcessor.prototype.saveFromTree.call(TouchChatProcessor, tree, outPath); + const exported = fs.readFileSync(outPath, 'utf8'); + expect(exported).toContain('pages'); + expect(exported).toContain('rootId'); }); }); diff --git a/test/validation.newFormats.test.ts b/test/validation.newFormats.test.ts index cc8d634..13aaaaf 100644 --- a/test/validation.newFormats.test.ts +++ b/test/validation.newFormats.test.ts @@ -83,8 +83,16 @@ describe('Validation - additional formats', () => { it('validates OBFSet bundles', async () => { const obfset = [ - { id: 'board1', buttons: [{ id: 'b1', label: 'Hi' }], grid: { rows: 1, columns: 1 } }, - { id: 'board2', buttons: [{ id: 'b2', label: 'There' }], grid: { rows: 1, columns: 1 } }, + { + id: 'board1', + buttons: [{ id: 'b1', label: 'Hi' }], + grid: { rows: 1, columns: 1 }, + }, + { + id: 'board2', + buttons: [{ id: 'b2', label: 'There' }], + grid: { rows: 1, columns: 1 }, + }, ]; const buffer = Buffer.from(JSON.stringify(obfset)); const result = await validateFileOrBuffer(buffer, defaultFileAdapter, 'bundle.obfset'); diff --git a/tsconfig.json b/tsconfig.json index 653b284..7a989d7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,5 @@ "forceConsistentCasingInFileNames": true }, "include": ["src/**/*"], - "exclude": [ - "node_modules", "dist", "examples", "test"] + "exclude": ["node_modules", "dist", "examples", "test"] }