Skip to content

Commit 159feb3

Browse files
committed
Add script to extract symbols with context from gridsets
Introduces extract-symbols-with-context.js, a Node.js script that processes Grid3 gridset files to extract all unique symbols along with their usage context. The script outputs a CSV file containing symbol IDs, cell labels, page names, vocabulary, actions, and other metadata, and provides a summary of symbol usage by library and action type.
1 parent a488c13 commit 159feb3

1 file changed

Lines changed: 255 additions & 0 deletions

File tree

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
#!/usr/bin/env node
2+
3+
const fs = require('fs');
4+
const path = require('path');
5+
const { GridsetProcessor } = require('../../dist/processors');
6+
7+
/**
8+
* Extract all unique symbols with their usage context from Grid3 gridsets
9+
* Output: CSV with columns: symbol-id, cell-label, page, vocab, cell-actions, button-id
10+
*
11+
* Usage:
12+
* node extract-symbols-with-context.js <gridset-file> [output.csv]
13+
*
14+
* Example:
15+
* node extract-symbols-with-context.js "/path/to/Super Core.gridset" "super-core-symbols.csv"
16+
*/
17+
18+
function getSymbolId(button) {
19+
// Construct symbol reference from symbol library and path
20+
if (button.symbolLibrary) {
21+
const lib = button.symbolLibrary;
22+
const symPath = button.symbolPath || '';
23+
return `[${lib}]${symPath}`;
24+
}
25+
// For embedded images, use the resolved entry path
26+
if (button.resolvedImageEntry) {
27+
return `embedded:${button.resolvedImageEntry}`;
28+
}
29+
// Fallback to image field
30+
if (button.image) {
31+
return `image:${button.image}`;
32+
}
33+
return '';
34+
}
35+
36+
function getCellActions(button) {
37+
const actions = [];
38+
39+
// Get semantic action info
40+
if (button.semanticAction) {
41+
const intent = button.semanticAction.intent || '';
42+
const text = button.semanticAction.text || button.message || '';
43+
44+
if (intent === 'SPEAK_TEXT' || intent === 'SPEAK_IMMEDIATE') {
45+
actions.push(`SPEAK:${text}`);
46+
} else if (intent === 'NAVIGATE_TO') {
47+
actions.push(`NAVIGATE:${button.semanticAction.targetId || button.targetPageId || ''}`);
48+
} else if (intent === 'INSERT_TEXT') {
49+
actions.push(`INSERT:${text}`);
50+
} else if (intent) {
51+
actions.push(`${intent}:${text}`);
52+
}
53+
}
54+
55+
// Fallback to legacy type/action
56+
if (actions.length === 0) {
57+
if (button.type === 'SPEAK' || button.action?.type === 'SPEAK') {
58+
const msg = button.message || button.action?.message || '';
59+
actions.push(`SPEAK:${msg}`);
60+
} else if (button.type === 'NAVIGATE' || button.action?.type === 'NAVIGATE') {
61+
const target = button.targetPageId || button.action?.targetPageId || '';
62+
actions.push(`NAVIGATE:${target}`);
63+
}
64+
}
65+
66+
return actions.join('; ') || '(none)';
67+
}
68+
69+
function extractSymbolUsage(gridsetFile, vocabName) {
70+
console.log(`Loading gridset: ${gridsetFile}`);
71+
72+
const proc = new GridsetProcessor();
73+
const tree = proc.loadIntoTree(gridsetFile);
74+
75+
const symbolMap = new Map(); // symbol-id -> array of usage entries
76+
77+
for (const pageId in tree.pages) {
78+
const page = tree.pages[pageId];
79+
80+
if (!page.buttons || page.buttons.length === 0) {
81+
continue;
82+
}
83+
84+
for (const button of page.buttons) {
85+
// Skip buttons without images/symbols
86+
if (!button.symbolLibrary && !button.resolvedImageEntry && !button.image) {
87+
continue;
88+
}
89+
90+
const symbolId = getSymbolId(button);
91+
if (!symbolId) {
92+
continue;
93+
}
94+
95+
const entry = {
96+
symbolId,
97+
cellLabel: button.label || '',
98+
page: page.name || pageId,
99+
vocab: vocabName,
100+
cellActions: getCellActions(button),
101+
buttonId: button.id,
102+
visibility: button.visibility || 'Visible',
103+
contentType: button.contentType || 'Normal'
104+
};
105+
106+
if (!symbolMap.has(symbolId)) {
107+
symbolMap.set(symbolId, []);
108+
}
109+
symbolMap.get(symbolId).push(entry);
110+
}
111+
}
112+
113+
return symbolMap;
114+
}
115+
116+
function generateCSV(symbolMap, outputFile) {
117+
const headers = ['symbol-id', 'cell-label', 'page', 'vocab', 'cell-actions', 'button-id', 'visibility', 'content-type'];
118+
const rows = [headers.join(',')];
119+
120+
// Sort by symbol-id
121+
const sortedSymbols = Array.from(symbolMap.entries()).sort((a, b) =>
122+
a[0].localeCompare(b[0])
123+
);
124+
125+
for (const [symbolId, usages] of sortedSymbols) {
126+
for (const usage of usages) {
127+
// Escape CSV fields
128+
const escape = (str) => {
129+
if (!str) return '""';
130+
const s = String(str).replace(/"/g, '""');
131+
return `"${s}"`;
132+
};
133+
134+
const row = [
135+
escape(usage.symbolId),
136+
escape(usage.cellLabel),
137+
escape(usage.page),
138+
escape(usage.vocab),
139+
escape(usage.cellActions),
140+
escape(usage.buttonId),
141+
escape(usage.visibility),
142+
escape(usage.contentType)
143+
];
144+
145+
rows.push(row.join(','));
146+
}
147+
}
148+
149+
const csv = rows.join('\n');
150+
fs.writeFileSync(outputFile, csv, 'utf8');
151+
console.log(`\nWrote ${sortedSymbols.length} unique symbols (${rows.length - 1} total usages) to ${outputFile}`);
152+
}
153+
154+
function generateSummary(symbolMap) {
155+
const summary = {
156+
totalUniqueSymbols: symbolMap.size,
157+
totalUsages: 0,
158+
byLibrary: {},
159+
byVocab: {},
160+
byAction: { SPEAK: 0, NAVIGATE: 0, OTHER: 0 }
161+
};
162+
163+
for (const [symbolId, usages] of symbolMap.entries()) {
164+
summary.totalUsages += usages.length;
165+
166+
// Count by library
167+
let library = 'unknown';
168+
if (symbolId.startsWith('[')) {
169+
const match = symbolId.match(/^\[([^\]]+)\]/);
170+
if (match) library = match[1];
171+
} else if (symbolId.startsWith('embedded:')) {
172+
library = 'embedded';
173+
} else if (symbolId.startsWith('image:')) {
174+
library = 'image-ref';
175+
}
176+
summary.byLibrary[library] = (summary.byLibrary[library] || 0) + 1;
177+
178+
// Count by vocab
179+
for (const usage of usages) {
180+
summary.byVocab[usage.vocab] = (summary.byVocab[usage.vocab] || 0) + 1;
181+
182+
// Count by action type
183+
if (usage.cellActions.startsWith('SPEAK:')) {
184+
summary.byAction.SPEAK++;
185+
} else if (usage.cellActions.startsWith('NAVIGATE:')) {
186+
summary.byAction.NAVIGATE++;
187+
} else {
188+
summary.byAction.OTHER++;
189+
}
190+
}
191+
}
192+
193+
return summary;
194+
}
195+
196+
function main() {
197+
const args = process.argv.slice(2);
198+
199+
if (args.length === 0) {
200+
console.log(`
201+
Usage: node extract-symbols-with-context.js <gridset-file> [output.csv] [vocab-name]
202+
203+
Arguments:
204+
gridset-file Path to the .gridset file to process
205+
output.csv Optional output CSV filename (default: symbols-output.csv)
206+
vocab-name Optional vocabulary name (default: extracted from filename)
207+
208+
Examples:
209+
node extract-symbols-with-context.js "/tmp/Super Core.gridset"
210+
node extract-symbols-with-context.js "/tmp/Aphasia Duo.gridset" "aphasia-duo-symbols.csv"
211+
node extract-symbols-with-context.js "/tmp/Voco Chat.gridset" "voco-chat-symbols.csv" "VocoChat"
212+
`);
213+
process.exit(1);
214+
}
215+
216+
const gridsetFile = args[0];
217+
const defaultVocab = path.basename(gridsetFile, '.gridset');
218+
const outputFile = args[1] || `${defaultVocab}-symbols.csv`;
219+
const vocabName = args[2] || defaultVocab;
220+
221+
// Check if file exists
222+
if (!fs.existsSync(gridsetFile)) {
223+
console.error(`Error: Gridset file not found: ${gridsetFile}`);
224+
process.exit(1);
225+
}
226+
227+
try {
228+
const symbolMap = extractSymbolUsage(gridsetFile, vocabName);
229+
generateCSV(symbolMap, outputFile);
230+
231+
const summary = generateSummary(symbolMap);
232+
console.log('\n=== Summary ===');
233+
console.log(`Unique symbols: ${summary.totalUniqueSymbols}`);
234+
console.log(`Total usages: ${summary.totalUsages}`);
235+
console.log('\nBy symbol library:');
236+
for (const [lib, count] of Object.entries(summary.byLibrary).sort((a, b) => b[1] - a[1])) {
237+
console.log(` ${lib}: ${count}`);
238+
}
239+
console.log('\nBy action type:');
240+
console.log(` SPEAK: ${summary.byAction.SPEAK}`);
241+
console.log(` NAVIGATE: ${summary.byAction.NAVIGATE}`);
242+
console.log(` OTHER: ${summary.byAction.OTHER}`);
243+
244+
} catch (error) {
245+
console.error('Error:', error.message);
246+
console.error(error.stack);
247+
process.exit(1);
248+
}
249+
}
250+
251+
if (require.main === module) {
252+
main();
253+
}
254+
255+
module.exports = { extractSymbolUsage, generateCSV, generateSummary };

0 commit comments

Comments
 (0)