@@ -17,6 +17,7 @@ import {
1717import plist , { PlistValue } from 'plist' ;
1818import fs from 'fs' ;
1919import path from 'path' ;
20+ import { ValidationFailureError , buildValidationResultFromMessage } from '../validation' ;
2021
2122interface ApplePanelsActionParameters {
2223 CharString ?: string ;
@@ -216,152 +217,195 @@ class ApplePanelsProcessor extends BaseProcessor {
216217 }
217218
218219 loadIntoTree ( filePathOrBuffer : string | Buffer ) : AACTree {
219- let content : string ;
220-
221- if ( Buffer . isBuffer ( filePathOrBuffer ) ) {
222- content = filePathOrBuffer . toString ( 'utf8' ) ;
223- } else if ( typeof filePathOrBuffer === 'string' ) {
224- // Check if it's a .ascconfig folder or a direct .plist file
225- if ( filePathOrBuffer . endsWith ( '.ascconfig' ) ) {
226- // Read from proper Apple Panels structure: *.ascconfig/Contents/Resources/PanelDefinitions.plist
227- const panelDefsPath = `${ filePathOrBuffer } /Contents/Resources/PanelDefinitions.plist` ;
228- if ( fs . existsSync ( panelDefsPath ) ) {
229- content = fs . readFileSync ( panelDefsPath , 'utf8' ) ;
220+ const filename =
221+ typeof filePathOrBuffer === 'string' ? path . basename ( filePathOrBuffer ) : 'upload.plist' ;
222+ let buffer : Buffer ;
223+
224+ try {
225+ if ( Buffer . isBuffer ( filePathOrBuffer ) ) {
226+ buffer = filePathOrBuffer ;
227+ } else if ( typeof filePathOrBuffer === 'string' ) {
228+ if ( filePathOrBuffer . endsWith ( '.ascconfig' ) ) {
229+ const panelDefsPath = `${ filePathOrBuffer } /Contents/Resources/PanelDefinitions.plist` ;
230+ if ( fs . existsSync ( panelDefsPath ) ) {
231+ buffer = fs . readFileSync ( panelDefsPath ) ;
232+ } else {
233+ const validation = buildValidationResultFromMessage ( {
234+ filename,
235+ filesize : 0 ,
236+ format : 'applepanels' ,
237+ message : `Apple Panels file not found: ${ panelDefsPath } ` ,
238+ type : 'missing' ,
239+ description : 'PanelDefinitions.plist' ,
240+ } ) ;
241+ throw new ValidationFailureError ( 'Apple Panels file not found' , validation ) ;
242+ }
230243 } else {
231- throw new Error ( `Apple Panels file not found: ${ panelDefsPath } ` ) ;
244+ buffer = fs . readFileSync ( filePathOrBuffer ) ;
232245 }
233246 } else {
234- // Fallback: treat as direct .plist file
235- content = fs . readFileSync ( filePathOrBuffer , 'utf8' ) ;
247+ const validation = buildValidationResultFromMessage ( {
248+ filename,
249+ filesize : 0 ,
250+ format : 'applepanels' ,
251+ message : 'Invalid input: expected string path or Buffer' ,
252+ type : 'input' ,
253+ description : 'Apple Panels input' ,
254+ } ) ;
255+ throw new ValidationFailureError ( 'Invalid Apple Panels input' , validation ) ;
236256 }
237- } else {
238- throw new Error ( 'Invalid input: expected string path or Buffer' ) ;
239- }
240257
241- const parsedData = plist . parse ( content ) as ApplePanelsParsedDocument ;
258+ const content = buffer . toString ( 'utf8' ) ;
259+ const parsedData = plist . parse ( content ) as ApplePanelsParsedDocument ;
242260
243- // Handle both old format (panels array) and new Apple Panels format (Panels dict)
244- let panelsData : ApplePanelsPanel [ ] = [ ] ;
245- if ( Array . isArray ( parsedData . panels ) ) {
246- panelsData = parsedData . panels . map ( ( panel , index ) => {
247- if ( isNormalizedPanel ( panel ) ) {
248- return panel ;
249- }
250- const panelData = panel || {
251- PanelObjects : [ ] ,
252- } ;
253- return normalizePanel ( panelData , `panel_${ index } ` ) ;
254- } ) ;
255- } else if ( parsedData . Panels ) {
256- const panelsDict = parsedData . Panels ;
257- panelsData = Object . keys ( panelsDict ) . map ( ( panelId ) => {
258- const rawPanel = panelsDict [ panelId ] || { PanelObjects : [ ] } ;
259- return normalizePanel ( rawPanel , panelId ) ;
260- } ) ;
261- }
262-
263- const data : ApplePanelsDocument = { panels : panelsData } ;
264- const tree = new AACTree ( ) ;
265- tree . metadata . format = 'applepanels' ;
266-
267- data . panels . forEach ( ( panel ) => {
268- const page = new AACPage ( {
269- id : panel . id ,
270- name : panel . name ,
271- grid : [ ] ,
272- buttons : [ ] ,
273- parentId : null ,
274- } ) ;
261+ let panelsData : ApplePanelsPanel [ ] = [ ] ;
262+ if ( Array . isArray ( parsedData . panels ) ) {
263+ panelsData = parsedData . panels . map ( ( panel , index ) => {
264+ if ( isNormalizedPanel ( panel ) ) {
265+ return panel ;
266+ }
267+ const panelData = panel || {
268+ PanelObjects : [ ] ,
269+ } ;
270+ return normalizePanel ( panelData , `panel_${ index } ` ) ;
271+ } ) ;
272+ } else if ( parsedData . Panels ) {
273+ const panelsDict = parsedData . Panels ;
274+ panelsData = Object . keys ( panelsDict ) . map ( ( panelId ) => {
275+ const rawPanel = panelsDict [ panelId ] || { PanelObjects : [ ] } ;
276+ return normalizePanel ( rawPanel , panelId ) ;
277+ } ) ;
278+ }
275279
276- // Create a 2D grid to track button positions
277- const gridLayout : ( AACButton | null ) [ ] [ ] = [ ] ;
278- const maxRows = 20 ; // Reasonable default for Apple Panels
279- const maxCols = 20 ;
280- for ( let r = 0 ; r < maxRows ; r ++ ) {
281- gridLayout [ r ] = new Array ( maxCols ) . fill ( null ) ;
280+ if ( panelsData . length === 0 ) {
281+ const validation = buildValidationResultFromMessage ( {
282+ filename,
283+ filesize : buffer . byteLength ,
284+ format : 'applepanels' ,
285+ message : 'No panels found in Apple Panels file' ,
286+ type : 'structure' ,
287+ description : 'Panels definition' ,
288+ } ) ;
289+ throw new ValidationFailureError ( 'Apple Panels has no panels' , validation ) ;
282290 }
283291
284- panel . buttons . forEach ( ( btn , idx ) => {
285- // Create semantic action from Apple Panels button
286- let semanticAction : AACSemanticAction | undefined ;
287-
288- if ( btn . targetPanel ) {
289- semanticAction = {
290- category : AACSemanticCategory . NAVIGATION ,
291- intent : AACSemanticIntent . NAVIGATE_TO ,
292- targetId : btn . targetPanel ,
293- platformData : {
294- applePanels : {
295- actionType : 'ActionOpenPanel' ,
296- parameters : { PanelID : `USER.${ btn . targetPanel } ` } ,
292+ const data : ApplePanelsDocument = { panels : panelsData } ;
293+ const tree = new AACTree ( ) ;
294+ tree . metadata . format = 'applepanels' ;
295+
296+ data . panels . forEach ( ( panel ) => {
297+ const page = new AACPage ( {
298+ id : panel . id ,
299+ name : panel . name ,
300+ grid : [ ] ,
301+ buttons : [ ] ,
302+ parentId : null ,
303+ } ) ;
304+
305+ const gridLayout : ( AACButton | null ) [ ] [ ] = [ ] ;
306+ const maxRows = 20 ;
307+ const maxCols = 20 ;
308+ for ( let r = 0 ; r < maxRows ; r ++ ) {
309+ gridLayout [ r ] = new Array ( maxCols ) . fill ( null ) ;
310+ }
311+
312+ panel . buttons . forEach ( ( btn , idx ) => {
313+ let semanticAction : AACSemanticAction | undefined ;
314+
315+ if ( btn . targetPanel ) {
316+ semanticAction = {
317+ category : AACSemanticCategory . NAVIGATION ,
318+ intent : AACSemanticIntent . NAVIGATE_TO ,
319+ targetId : btn . targetPanel ,
320+ platformData : {
321+ applePanels : {
322+ actionType : 'ActionOpenPanel' ,
323+ parameters : { PanelID : `USER.${ btn . targetPanel } ` } ,
324+ } ,
297325 } ,
298- } ,
299- fallback : {
300- type : 'NAVIGATE' ,
301- targetPageId : btn . targetPanel ,
302- } ,
303- } ;
304- } else {
305- semanticAction = {
306- category : AACSemanticCategory . COMMUNICATION ,
307- intent : AACSemanticIntent . SPEAK_TEXT ,
308- text : btn . message || btn . label ,
309- platformData : {
310- applePanels : {
311- actionType : 'ActionPressKeyCharSequence' ,
312- parameters : {
313- CharString : btn . message || btn . label || '' ,
314- isStickyKey : false ,
326+ fallback : {
327+ type : 'NAVIGATE' ,
328+ targetPageId : btn . targetPanel ,
329+ } ,
330+ } ;
331+ } else {
332+ semanticAction = {
333+ category : AACSemanticCategory . COMMUNICATION ,
334+ intent : AACSemanticIntent . SPEAK_TEXT ,
335+ text : btn . message || btn . label ,
336+ platformData : {
337+ applePanels : {
338+ actionType : 'ActionPressKeyCharSequence' ,
339+ parameters : {
340+ CharString : btn . message || btn . label || '' ,
341+ isStickyKey : false ,
342+ } ,
315343 } ,
316344 } ,
317- } ,
318- fallback : {
319- type : 'SPEAK' ,
320- message : btn . message || btn . label ,
321- } ,
322- } ;
323- }
345+ fallback : {
346+ type : 'SPEAK' ,
347+ message : btn . message || btn . label ,
348+ } ,
349+ } ;
350+ }
324351
325- const button = new AACButton ( {
326- id : `${ panel . id } _btn_${ idx } ` ,
327- label : btn . label ,
328- message : btn . message || btn . label ,
329- targetPageId : btn . targetPanel ,
330- semanticAction : semanticAction ,
331- style : {
332- backgroundColor : btn . DisplayColor ,
333- fontSize : btn . FontSize ,
334- fontWeight : btn . DisplayImageWeight === 'bold' ? 'bold' : 'normal' ,
335- } ,
336- } ) ;
337- page . addButton ( button ) ;
338-
339- // Place button in grid layout using Rect position data
340- if ( btn . Rect ) {
341- const rect = this . parseRect ( btn . Rect ) ;
342- if ( rect ) {
343- const gridPos = this . pixelToGrid ( rect . x , rect . y ) ;
344- const gridWidth = Math . max ( 1 , Math . ceil ( rect . width / 25 ) ) ;
345- const gridHeight = Math . max ( 1 , Math . ceil ( rect . height / 25 ) ) ;
346-
347- // Place button in grid (handle width/height span)
348- for ( let r = gridPos . gridY ; r < gridPos . gridY + gridHeight && r < maxRows ; r ++ ) {
349- for ( let c = gridPos . gridX ; c < gridPos . gridX + gridWidth && c < maxCols ; c ++ ) {
350- if ( gridLayout [ r ] && gridLayout [ r ] [ c ] === null ) {
351- gridLayout [ r ] [ c ] = button ;
352+ const button = new AACButton ( {
353+ id : `${ panel . id } _btn_${ idx } ` ,
354+ label : btn . label ,
355+ message : btn . message || btn . label ,
356+ targetPageId : btn . targetPanel ,
357+ semanticAction : semanticAction ,
358+ style : {
359+ backgroundColor : btn . DisplayColor ,
360+ fontSize : btn . FontSize ,
361+ fontWeight : btn . DisplayImageWeight === 'bold' ? 'bold' : 'normal' ,
362+ } ,
363+ } ) ;
364+ page . addButton ( button ) ;
365+
366+ if ( btn . Rect ) {
367+ const rect = this . parseRect ( btn . Rect ) ;
368+ if ( rect ) {
369+ const gridPos = this . pixelToGrid ( rect . x , rect . y ) ;
370+ const gridWidth = Math . max ( 1 , Math . ceil ( rect . width / 25 ) ) ;
371+ const gridHeight = Math . max ( 1 , Math . ceil ( rect . height / 25 ) ) ;
372+
373+ for ( let r = gridPos . gridY ; r < gridPos . gridY + gridHeight && r < maxRows ; r ++ ) {
374+ for ( let c = gridPos . gridX ; c < gridPos . gridX + gridWidth && c < maxCols ; c ++ ) {
375+ if ( gridLayout [ r ] && gridLayout [ r ] [ c ] === null ) {
376+ gridLayout [ r ] [ c ] = button ;
377+ }
352378 }
353379 }
354380 }
355381 }
356- }
357- } ) ;
382+ } ) ;
358383
359- // Set the page's grid layout
360- page . grid = gridLayout ;
361- tree . addPage ( page ) ;
362- } ) ;
384+ page . grid = gridLayout ;
385+ tree . addPage ( page ) ;
386+ } ) ;
363387
364- return tree ;
388+ return tree ;
389+ } catch ( err : any ) {
390+ if ( err instanceof ValidationFailureError ) {
391+ throw err ;
392+ }
393+ const validation = buildValidationResultFromMessage ( {
394+ filename,
395+ filesize : Buffer . isBuffer ( filePathOrBuffer )
396+ ? filePathOrBuffer . byteLength
397+ : typeof filePathOrBuffer === 'string'
398+ ? fs . existsSync ( filePathOrBuffer )
399+ ? fs . statSync ( filePathOrBuffer ) . size
400+ : 0
401+ : 0 ,
402+ format : 'applepanels' ,
403+ message : err ?. message || 'Failed to parse Apple Panels file' ,
404+ type : 'parse' ,
405+ description : 'Parse Apple Panels plist' ,
406+ } ) ;
407+ throw new ValidationFailureError ( 'Failed to load Apple Panels file' , validation , err ) ;
408+ }
365409 }
366410
367411 processTexts (
0 commit comments