@@ -1292,9 +1292,12 @@ class GridsetProcessor extends BaseProcessor {
12921292 break ;
12931293
12941294 case 'Jump.ToKeyboard' : {
1295- // Navigate to the set keyboard if we found one in settings
1295+ // Prefer explicit keyboard page metadata when available.
1296+ // Some Gridsets resolve the keyboard page in metadata
1297+ // without preserving tree.keyboardGridName during parse.
12961298 const keyboardGridName = ( tree as any ) . keyboardGridName as string ;
1297- const keyboardPageId = gridNameToIdMap . get ( keyboardGridName ) ;
1299+ const keyboardPageId =
1300+ tree . metadata ?. defaultKeyboardPageId || gridNameToIdMap . get ( keyboardGridName ) ;
12981301 if ( keyboardPageId && ! navigationTarget ) {
12991302 navigationTarget = keyboardPageId ;
13001303 }
@@ -1900,6 +1903,7 @@ class GridsetProcessor extends BaseProcessor {
19001903 settingsData ?. gridSetSettings ?. keyboardGrid ||
19011904 settingsData ?. GridsetSettings ?. KeyboardGrid ;
19021905 if ( keyboardGridName && typeof keyboardGridName === 'string' ) {
1906+ ( tree as any ) . keyboardGridName = keyboardGridName ;
19031907 metadata . defaultKeyboardPageId = gridNameToIdMap . get ( keyboardGridName ) ;
19041908 }
19051909 }
@@ -1909,6 +1913,24 @@ class GridsetProcessor extends BaseProcessor {
19091913
19101914 // Set metadata on tree
19111915 tree . metadata = metadata ;
1916+ if ( metadata . defaultKeyboardPageId ) {
1917+ Object . values ( tree . pages ) . forEach ( ( page ) => {
1918+ page . buttons . forEach ( ( button ) => {
1919+ if (
1920+ button ?. semanticAction ?. platformData ?. grid3 ?. commandId === 'Jump.ToKeyboard' &&
1921+ ! button . targetPageId
1922+ ) {
1923+ button . targetPageId = metadata . defaultKeyboardPageId ;
1924+ if ( button . semanticAction ) {
1925+ button . semanticAction . targetId = metadata . defaultKeyboardPageId ;
1926+ if ( button . semanticAction . fallback ?. type === 'NAVIGATE' ) {
1927+ button . semanticAction . fallback . targetPageId = metadata . defaultKeyboardPageId ;
1928+ }
1929+ }
1930+ }
1931+ } ) ;
1932+ } ) ;
1933+ }
19121934
19131935 return tree ;
19141936 }
@@ -2494,6 +2516,152 @@ class GridsetProcessor extends BaseProcessor {
24942516 } ;
24952517 }
24962518
2519+ /**
2520+ * Save a modified tree while preserving all original files (settings, images, assets)
2521+ * This method only updates the grid.xml files for pages in the tree, keeping everything else intact.
2522+ *
2523+ * @param originalPath - Path to the original gridset file
2524+ * @param tree - Modified AACTree with pages to save
2525+ * @param outputPath - Path where the modified gridset should be saved
2526+ */
2527+ async saveModifiedTree ( originalPath : string , tree : AACTree , outputPath : string ) : Promise < void > {
2528+ const { readBinaryFromInput, writeBinaryToPath } = this . options . fileAdapter ;
2529+
2530+ if ( Object . keys ( tree . pages ) . length === 0 ) {
2531+ // Empty tree, just copy the original
2532+ const originalBuffer = await readBinaryFromInput ( originalPath ) ;
2533+ await writeBinaryToPath ( outputPath , originalBuffer ) ;
2534+ return ;
2535+ }
2536+
2537+ const AdmZip = ( await import ( 'adm-zip' ) ) . default ;
2538+ const originalZip = new AdmZip ( originalPath ) ;
2539+ const outputZip = new AdmZip ( ) ;
2540+
2541+ // Collect styles from the tree for grid.xml files
2542+ const uniqueStyles = new Map < string , { id : string ; style : AACStyle } > ( ) ;
2543+ let styleIdCounter = 1 ;
2544+
2545+ const addStyle = ( style : AACStyle | undefined ) : string => {
2546+ if ( ! style ) return '' ;
2547+ const normalizedStyle : AACStyle = { ...style } ;
2548+ const styleKey = JSON . stringify ( normalizedStyle ) ;
2549+ const existing = uniqueStyles . get ( styleKey ) ;
2550+ if ( existing ) return existing . id ;
2551+
2552+ const styleId = `Style${ styleIdCounter ++ } ` ;
2553+ uniqueStyles . set ( styleKey , { id : styleId , style : normalizedStyle } ) ;
2554+ return styleId ;
2555+ } ;
2556+
2557+ // Collect all styles from pages and buttons
2558+ Object . values ( tree . pages ) . forEach ( ( page ) => {
2559+ addStyle ( page . style ) ;
2560+ page . buttons . forEach ( ( button ) => {
2561+ addStyle ( button . style ) ;
2562+ } ) ;
2563+ } ) ;
2564+
2565+ // Track which grid files we're modifying
2566+ const modifiedGridFiles = new Set < string > ( ) ;
2567+
2568+ // Generate grid.xml files for pages in the tree
2569+ const newGridFiles = new Map < string , string > ( ) ;
2570+
2571+ for ( const page of Object . values ( tree . pages ) ) {
2572+ const gridPath = `Grids/${ page . name } /grid.xml` ;
2573+ modifiedGridFiles . add ( gridPath ) ;
2574+
2575+ // Build the grid XML content
2576+ const gridData = {
2577+ Grid : {
2578+ '@_xmlns:xsi' : 'http://www.w3.org/2001/XMLSchema-instance' ,
2579+ GridGuid : page . id ,
2580+ ColumnDefinitions : this . calculateColumnDefinitions ( page ) ,
2581+ RowDefinitions : this . calculateRowDefinitions ( page , false ) ,
2582+ AutoContentCommands : '' ,
2583+ Cells :
2584+ page . buttons . length > 0
2585+ ? {
2586+ Cell : this . filterPageButtons ( page . buttons ) . map ( ( button , btnIndex ) => {
2587+ const buttonStyleId = button . style ? addStyle ( button . style ) : '' ;
2588+ const position = this . findButtonPosition ( page , button , btnIndex ) ;
2589+
2590+ const captionAndImage : Record < string , unknown > = {
2591+ Caption : button . label || '' ,
2592+ } ;
2593+
2594+ // Handle image references
2595+ if ( button . image ) {
2596+ captionAndImage . Image = `${ button . image } ` ;
2597+ }
2598+
2599+ const cell : Record < string , unknown > = {
2600+ '@_Column' : position . x ,
2601+ '@_Row' : position . y ,
2602+ captionAndImage,
2603+ } ;
2604+
2605+ if ( position . columnSpan > 1 ) {
2606+ cell [ '@_ColumnSpan' ] = position . columnSpan ;
2607+ }
2608+ if ( position . rowSpan > 1 ) {
2609+ cell [ '@_RowSpan' ] = position . rowSpan ;
2610+ }
2611+
2612+ if ( buttonStyleId ) {
2613+ cell . CellStyle = buttonStyleId ;
2614+ }
2615+
2616+ if ( button . message && button . message !== button . label ) {
2617+ // Use spoken message if different from label
2618+ const spoken = button . message ;
2619+ const cellContent : Record < string , unknown > = {
2620+ spoken,
2621+ type : 'text' ,
2622+ } ;
2623+ cell [ 'ContentCell' ] = cellContent ;
2624+ }
2625+
2626+ return cell ;
2627+ } ) ,
2628+ }
2629+ : undefined ,
2630+ } ,
2631+ } ;
2632+
2633+ const gridBuilder = new XMLBuilder ( {
2634+ ignoreAttributes : false ,
2635+ format : true ,
2636+ indentBy : ' ' ,
2637+ suppressEmptyNode : true ,
2638+ } ) ;
2639+
2640+ newGridFiles . set ( gridPath , gridBuilder . build ( gridData ) ) ;
2641+ }
2642+
2643+ // Copy all files from original zip, replacing modified grid files
2644+ for ( const entry of originalZip . getEntries ( ) ) {
2645+ if ( entry . isDirectory ) continue ;
2646+
2647+ // Skip grid.xml files that we're modifying
2648+ if ( modifiedGridFiles . has ( entry . entryName ) ) {
2649+ const newContent = newGridFiles . get ( entry . entryName ) ;
2650+ if ( newContent ) {
2651+ outputZip . addFile ( entry . entryName , Buffer . from ( newContent , 'utf8' ) ) ;
2652+ }
2653+ continue ;
2654+ }
2655+
2656+ // Copy all other files as-is
2657+ outputZip . addFile ( entry . entryName , entry . getData ( ) ) ;
2658+ }
2659+
2660+ // Write the output ZIP
2661+ const outputBuffer = outputZip . toBuffer ( ) ;
2662+ await writeBinaryToPath ( outputPath , outputBuffer ) ;
2663+ }
2664+
24972665 // Helper method to find button position with span information
24982666 private findButtonPosition (
24992667 page : AACPage ,
0 commit comments