@@ -82,7 +82,14 @@ async function main() {
8282 . usage ( '<gridset> [options]' )
8383 . argument ( '<gridset>' , 'Path to .gridset file' )
8484 . option ( '--page-name <substring>' , 'Only update pages whose name includes this substring' )
85- . option ( '--layout <layoutId>' , 'Target keyboard layout id' , 'ar-arabic-101' )
85+ . option ( '--all-letter-pages' , 'Update all pages containing Action.Letter commands' )
86+ . option ( '--layout <layoutId>' , 'Target keyboard layout id' )
87+ . option ( '--target-lang <code>' , 'Pick target layout by language prefix (example: ar, en)' )
88+ . option (
89+ '--target-layout-index <number>' ,
90+ '1-based index when --target-lang returns multiple layouts'
91+ )
92+ . option ( '--list-target-layouts' , 'List layouts matching --target-lang and exit' )
8693 . option ( '--source-layout <layoutId>' , 'Source keyboard layout id' , 'en-us' )
8794 . option ( '--output <path>' , 'Output .gridset path (default: add -keyboard-replaced)' )
8895 . option ( '--flip-keys-for-grid-rl' , 'Mirror key positions before mapping' )
@@ -97,47 +104,107 @@ async function main() {
97104 return ;
98105 }
99106
100- if ( ! options . pageName ) {
101- console . error ( 'Error: --page-name is required to avoid accidental global changes .' ) ;
107+ if ( ! options . pageName && ! options . allLetterPages ) {
108+ console . error ( 'Error: provide --page-name <substring> or --all-letter-pages .' ) ;
102109 process . exit ( 1 ) ;
103110 }
104111
112+ if ( options . pageName && options . allLetterPages ) {
113+ console . log ( 'Both --page-name and --all-letter-pages were provided; using --all-letter-pages.' ) ;
114+ }
115+
105116 const outputPath =
106117 options . output || gridsetPath . replace ( / \. g r i d s e t $ / i, '-keyboard-replaced.gridset' ) ;
107118
108119 let loadKeyboard ;
120+ let getAvailableLayouts ;
109121 let extractLayers ;
110122 try {
111- ( { loadKeyboard, extractLayers } = require ( 'worldalphabets' ) ) ;
123+ ( { loadKeyboard, getAvailableLayouts , extractLayers } = require ( 'worldalphabets' ) ) ;
112124 } catch ( error ) {
113125 console . error ( 'Missing dependency: worldalphabets' ) ;
114126 console . error ( 'Install it for this script with: npm install --no-save worldalphabets' ) ;
115127 process . exit ( 1 ) ;
116128 }
117129
130+ let targetLayoutId = options . layout || null ;
131+ if ( ! targetLayoutId && options . targetLang ) {
132+ const languageCode = String ( options . targetLang ) . trim ( ) . toLowerCase ( ) ;
133+ const layouts = await getAvailableLayouts ( ) ;
134+ const matchingLayouts = layouts
135+ . filter ( ( id ) => typeof id === 'string' )
136+ . filter ( ( id ) => id . toLowerCase ( ) . startsWith ( `${ languageCode } -` ) )
137+ . sort ( ( a , b ) => a . localeCompare ( b ) ) ;
138+
139+ if ( matchingLayouts . length === 0 ) {
140+ console . error ( `No keyboard layouts found for --target-lang ${ languageCode } ` ) ;
141+ process . exit ( 1 ) ;
142+ }
143+
144+ if ( options . listTargetLayouts ) {
145+ console . log ( `Layouts for ${ languageCode } :` ) ;
146+ matchingLayouts . forEach ( ( layoutId , index ) => {
147+ console . log ( `${ index + 1 } . ${ layoutId } ` ) ;
148+ } ) ;
149+ return ;
150+ }
151+
152+ if ( options . targetLayoutIndex !== undefined ) {
153+ const parsedIndex = parseInt ( String ( options . targetLayoutIndex ) , 10 ) ;
154+ if ( Number . isNaN ( parsedIndex ) || parsedIndex < 1 || parsedIndex > matchingLayouts . length ) {
155+ console . error (
156+ `Invalid --target-layout-index ${ options . targetLayoutIndex } . Valid range: 1-${ matchingLayouts . length } `
157+ ) ;
158+ process . exit ( 1 ) ;
159+ }
160+ targetLayoutId = matchingLayouts [ parsedIndex - 1 ] ;
161+ } else {
162+ targetLayoutId = matchingLayouts [ 0 ] ;
163+ console . log (
164+ `Using first layout for ${ languageCode } : ${ targetLayoutId } (use --target-layout-index to select another)`
165+ ) ;
166+ }
167+ } else if ( targetLayoutId && options . targetLang ) {
168+ console . log ( 'Both --layout and --target-lang were provided; using --layout.' ) ;
169+ } else if ( options . listTargetLayouts ) {
170+ console . error ( 'Error: --list-target-layouts requires --target-lang.' ) ;
171+ process . exit ( 1 ) ;
172+ }
173+
174+ if ( ! targetLayoutId ) {
175+ targetLayoutId = 'ar-arabic-101' ;
176+ console . log ( `No target layout provided. Using default: ${ targetLayoutId } ` ) ;
177+ }
178+
118179 console . log ( 'Loading layouts...' ) ;
119180 const sourceLayout = await loadKeyboard ( options . sourceLayout ) ;
120- const targetLayout = await loadKeyboard ( options . layout ) ;
181+ const targetLayout = await loadKeyboard ( targetLayoutId ) ;
121182 const { layers : sourceLayers , charToCodes } = buildLayerMaps ( sourceLayout , extractLayers ) ;
122183 const { layers : targetLayers } = buildLayerMaps ( targetLayout , extractLayers ) ;
123184 const positionMap = buildPositionMap ( sourceLayout ) ;
124185
125186 console . log ( 'Loading gridset...' ) ;
126187 const zip = new AdmZip ( gridsetPath ) ;
127188 const entries = zip . getEntries ( ) ;
128- const nameFilter = String ( options . pageName ) . toLowerCase ( ) ;
189+ const useAllLetterPages = Boolean ( options . allLetterPages ) ;
190+ const nameFilter = useAllLetterPages ? '' : String ( options . pageName ) . toLowerCase ( ) ;
129191 const parser = new XMLParser ( { ignoreAttributes : false } ) ;
130- const builder = new XMLBuilder ( { ignoreAttributes : false , suppressEmptyNode : true } ) ;
192+ const builder = new XMLBuilder ( {
193+ ignoreAttributes : false ,
194+ suppressEmptyNode : true ,
195+ suppressBooleanAttributes : false
196+ } ) ;
131197
132198 let pagesMatched = 0 ;
199+ let pagesWithLetterButtons = 0 ;
133200 let buttonsUpdated = 0 ;
134201 let buttonsSkipped = 0 ;
135202
136203 for ( const entry of entries ) {
137204 if ( entry . isDirectory ) continue ;
138205 if ( ! entry . entryName . startsWith ( 'Grids/' ) || ! entry . entryName . endsWith ( '/grid.xml' ) ) continue ;
139206 const pageName = entry . entryName . slice ( 'Grids/' . length , - '/grid.xml' . length ) ;
140- if ( ! pageName . toLowerCase ( ) . includes ( nameFilter ) ) continue ;
207+ if ( ! useAllLetterPages && ! pageName . toLowerCase ( ) . includes ( nameFilter ) ) continue ;
141208
142209 pagesMatched += 1 ;
143210
@@ -154,6 +221,7 @@ async function main() {
154221 }
155222
156223 let gridUpdated = 0 ;
224+ let gridHasLetterButtons = false ;
157225
158226 for ( const cell of cells ) {
159227 const content = cell . Content || cell . content ;
@@ -171,7 +239,7 @@ async function main() {
171239 }
172240 if ( ! targetCommand ) continue ;
173241
174- const params = asArray ( targetCommand . Parameter ) ;
242+ const params = asArray ( targetCommand . Parameter || targetCommand . parameter ) ;
175243 let letterParam = null ;
176244 for ( const param of params ) {
177245 const key = String ( getAttr ( param , [ '@_Key' , '@_key' ] ) || '' ) . toLowerCase ( ) ;
@@ -181,6 +249,7 @@ async function main() {
181249 }
182250 }
183251 if ( ! letterParam ) continue ;
252+ gridHasLetterButtons = true ;
184253
185254 const original = normalizeChar ( getAttr ( letterParam , [ '#text' ] ) || '' ) ;
186255 if ( ! original || original . length !== 1 ) {
@@ -228,13 +297,16 @@ async function main() {
228297 buttonsUpdated += 1 ;
229298 }
230299
300+ if ( gridHasLetterButtons ) pagesWithLetterButtons += 1 ;
301+
231302 if ( gridUpdated > 0 ) {
232303 const rebuilt = builder . build ( parsed ) ;
233304 zip . updateFile ( entry . entryName , Buffer . from ( rebuilt , 'utf8' ) ) ;
234305 }
235306 }
236307
237308 console . log ( `Pages matched: ${ pagesMatched } ` ) ;
309+ console . log ( `Pages with Action.Letter buttons: ${ pagesWithLetterButtons } ` ) ;
238310 console . log ( `Buttons updated: ${ buttonsUpdated } ` ) ;
239311 console . log ( `Buttons skipped: ${ buttonsSkipped } ` ) ;
240312
0 commit comments