Skip to content

Commit f6a957b

Browse files
committed
Add gridset to markdown conversion script
Introduces a TypeScript script to convert gridset files (.gridset or .gridsetx) into markdown documentation. The script loads a gridset, processes its pages and buttons, and outputs a structured markdown file with navigation, grid layouts, and button details.
1 parent 2d15f75 commit f6a957b

1 file changed

Lines changed: 313 additions & 0 deletions

File tree

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
#!/usr/bin/env ts-node
2+
3+
/**
4+
* Gridset to Markdown Converter Example
5+
*
6+
* This example demonstrates how to:
7+
* 1. Load a gridset file (.gridset or .gridsetx)
8+
* 2. Navigate through pages in the gridset
9+
* 3. Convert a page to markdown format
10+
*/
11+
12+
import { GridsetProcessor } from '../src/processors/gridsetProcessor';
13+
import { AACTree, AACPage, AACButton } from '../src/core/treeStructure';
14+
import fs from 'fs';
15+
import path from 'path';
16+
17+
/**
18+
* Convert a single button to markdown format
19+
*/
20+
function buttonToMarkdown(button: AACButton, includeIndex: boolean = false): string {
21+
const parts: string[] = [];
22+
23+
// Button index/position
24+
if (includeIndex && (button.x !== undefined || button.y !== undefined)) {
25+
parts.push(`**[${button.x ?? '?'}, ${button.y ?? '?'}]**`);
26+
}
27+
28+
// Button label
29+
const label = button.label || '(unnamed)';
30+
parts.push(`### ${label}`);
31+
32+
// Button message/speech
33+
if (button.message && button.message !== label) {
34+
parts.push(`**Message:** "${button.message}"`);
35+
}
36+
37+
// Button type/action
38+
if (button.type) {
39+
parts.push(`**Type:** ${button.type}`);
40+
}
41+
42+
// Navigation target
43+
if (button.type === 'NAVIGATE' && button.targetPageId) {
44+
parts.push(`**Target:** \`${button.targetPageId}\``);
45+
}
46+
47+
// Additional styling info
48+
const styleParts: string[] = [];
49+
if (button.style?.backgroundColor) {
50+
styleParts.push(`bg: ${button.style.backgroundColor}`);
51+
}
52+
if (button.style?.fontColor) {
53+
styleParts.push(`text: ${button.style.fontColor}`);
54+
}
55+
if (styleParts.length > 0) {
56+
parts.push(`**Style:** ${styleParts.join(', ')}`);
57+
}
58+
59+
// Symbol library reference
60+
if (button.symbolLibrary) {
61+
parts.push(`**Symbol:** \`${button.symbolLibrary}${button.symbolPath ? ':' + button.symbolPath : ''}\``);
62+
}
63+
64+
return parts.join('\n') + '\n';
65+
}
66+
67+
/**
68+
* Convert a page to markdown format
69+
*/
70+
function pageToMarkdown(page: AACPage, tree?: AACTree): string {
71+
const lines: string[] = [];
72+
73+
// Page header
74+
lines.push(`# ${page.name}`);
75+
lines.push('');
76+
lines.push(`**Page ID:** \`${page.id}\``);
77+
78+
// Parent page info
79+
if (page.parentId && tree) {
80+
const parent = tree.pages[page.parentId];
81+
if (parent) {
82+
lines.push(`**Parent:** [${parent.name}](#${parent.name.toLowerCase().replace(/\s+/g, '-')})`);
83+
}
84+
}
85+
86+
// Grid dimensions
87+
if (page.grid && page.grid.length > 0) {
88+
const rows = page.grid.length;
89+
const cols = Math.max(...page.grid.map(row => row?.length || 0));
90+
lines.push(`**Grid Size:** ${cols} columns × ${rows} rows`);
91+
}
92+
93+
lines.push('');
94+
95+
// Child pages
96+
if (tree) {
97+
const children = Object.values(tree.pages).filter(p => p.parentId === page.id);
98+
if (children.length > 0) {
99+
lines.push('## Child Pages');
100+
lines.push('');
101+
children.forEach(child => {
102+
lines.push(`- [${child.name}](#${child.name.toLowerCase().replace(/\s+/g, '-')})`);
103+
});
104+
lines.push('');
105+
}
106+
}
107+
108+
// Buttons grid (if available)
109+
if (page.grid && page.grid.length > 0) {
110+
lines.push('## Grid Layout');
111+
lines.push('');
112+
lines.push('```\n'); // Start code block for visual representation
113+
114+
for (let y = 0; y < page.grid.length; y++) {
115+
const row = page.grid[y];
116+
if (!row) continue;
117+
118+
const rowLabels: string[] = [];
119+
for (let x = 0; x < row.length; x++) {
120+
const button = row[x];
121+
if (button) {
122+
const label = (button.label || '(empty)').substring(0, 12).padEnd(12);
123+
rowLabels.push(label);
124+
} else {
125+
rowLabels.push(''.padEnd(12));
126+
}
127+
}
128+
lines.push(rowLabels.join(' │ '));
129+
}
130+
131+
lines.push('\n```\n'); // End code block
132+
}
133+
134+
// Buttons detail
135+
lines.push('## Buttons');
136+
lines.push('');
137+
138+
if (page.buttons.length === 0) {
139+
lines.push('*No buttons on this page*\n');
140+
} else {
141+
// Group buttons by type for better organization
142+
const speakButtons = page.buttons.filter(b => b.type === 'SPEAK');
143+
const navigateButtons = page.buttons.filter(b => b.type === 'NAVIGATE');
144+
const otherButtons = page.buttons.filter(b => b.type !== 'SPEAK' && b.type !== 'NAVIGATE');
145+
146+
if (speakButtons.length > 0) {
147+
lines.push('### Speech Buttons');
148+
lines.push('');
149+
speakButtons.forEach(button => {
150+
lines.push(buttonToMarkdown(button));
151+
});
152+
}
153+
154+
if (navigateButtons.length > 0) {
155+
lines.push('### Navigation Buttons');
156+
lines.push('');
157+
navigateButtons.forEach(button => {
158+
lines.push(buttonToMarkdown(button));
159+
});
160+
}
161+
162+
if (otherButtons.length > 0) {
163+
lines.push('### Other Buttons');
164+
lines.push('');
165+
otherButtons.forEach(button => {
166+
lines.push(buttonToMarkdown(button));
167+
});
168+
}
169+
}
170+
171+
return lines.join('\n') + '\n';
172+
}
173+
174+
/**
175+
* Convert entire tree to markdown with navigation
176+
*/
177+
function treeToMarkdown(tree: AACTree, options: {
178+
includeAllPages?: boolean;
179+
maxDepth?: number;
180+
startPageId?: string;
181+
} = {}): string {
182+
const { includeAllPages = true, maxDepth = 3, startPageId } = options;
183+
const lines: string[] = [];
184+
185+
// Document header
186+
lines.push('# Gridset Documentation');
187+
lines.push('');
188+
lines.push(`This document contains ${Object.keys(tree.pages).length} pages from the gridset.`);
189+
lines.push('');
190+
191+
// Table of Contents
192+
if (includeAllPages) {
193+
lines.push('## Table of Contents');
194+
lines.push('');
195+
addPagesToTOC(lines, tree, startPageId || tree.rootId || '', 0, maxDepth);
196+
lines.push('');
197+
lines.push('---');
198+
lines.push('');
199+
}
200+
201+
// Page content
202+
if (includeAllPages) {
203+
const visited = new Set<string>();
204+
205+
const addPage = (pageId: string, depth: number) => {
206+
if (visited.has(pageId) || depth > maxDepth) return;
207+
visited.add(pageId);
208+
209+
const page = tree.pages[pageId];
210+
if (!page) return;
211+
212+
lines.push(pageToMarkdown(page, tree));
213+
lines.push('---');
214+
lines.push('');
215+
216+
// Recursively add child pages
217+
Object.values(tree.pages)
218+
.filter(p => p.parentId === pageId)
219+
.forEach(child => addPage(child.id, depth + 1));
220+
};
221+
222+
addPage(startPageId || tree.rootId || '', 0);
223+
}
224+
225+
return lines.join('\n');
226+
}
227+
228+
/**
229+
* Helper to add pages to table of contents
230+
*/
231+
function addPagesToTOC(
232+
lines: string[],
233+
tree: AACTree,
234+
pageId: string,
235+
depth: number,
236+
maxDepth: number
237+
): void {
238+
if (depth > maxDepth) return;
239+
240+
const page = tree.pages[pageId];
241+
if (!page) return;
242+
243+
const indent = ' '.repeat(depth);
244+
const anchor = page.name.toLowerCase().replace(/\s+/g, '-');
245+
lines.push(`${indent}- [${page.name}](#${anchor})`);
246+
247+
// Add children
248+
Object.values(tree.pages)
249+
.filter(p => p.parentId === pageId)
250+
.forEach(child => addPagesToTOC(lines, tree, child.id, depth + 1, maxDepth));
251+
}
252+
253+
/**
254+
* Main demo function
255+
*/
256+
async function main() {
257+
console.log('📄 Gridset to Markdown Converter\n');
258+
259+
// Get gridset file path from command line or use example
260+
const gridsetPath = process.argv[2] || path.join(__dirname, 'example.gridset');
261+
262+
if (!fs.existsSync(gridsetPath)) {
263+
console.error(`❌ File not found: ${gridsetPath}`);
264+
console.log('\nUsage: ts-node gridset-to-markdown.ts <path-to-gridset>');
265+
process.exit(1);
266+
}
267+
268+
console.log(`📁 Loading: ${gridsetPath}`);
269+
270+
try {
271+
// Create processor and load the gridset
272+
const processor = new GridsetProcessor();
273+
const tree = processor.loadIntoTree(gridsetPath);
274+
275+
console.log(`✅ Loaded ${Object.keys(tree.pages).length} pages`);
276+
console.log(`🏠 Root page: ${tree.rootId ? tree.pages[tree.rootId]?.name : 'None'}`);
277+
278+
// Generate markdown for the entire tree
279+
console.log('\n📝 Generating markdown...');
280+
const markdown = treeToMarkdown(tree, {
281+
includeAllPages: true,
282+
maxDepth: 3,
283+
startPageId: tree.rootId || undefined
284+
});
285+
286+
// Save to file
287+
const outputPath = gridsetPath.replace(/\.(gridsetx?)/, '.md');
288+
fs.writeFileSync(outputPath, markdown, 'utf8');
289+
console.log(`💾 Saved to: ${outputPath}`);
290+
291+
// Show preview of root page
292+
if (tree.rootId && tree.pages[tree.rootId]) {
293+
console.log('\n--- Preview of Root Page ---\n');
294+
const rootPagePreview = pageToMarkdown(tree.pages[tree.rootId], tree);
295+
console.log(rootPagePreview.split('\n').slice(0, 30).join('\n'));
296+
console.log('...\n');
297+
}
298+
299+
} catch (error) {
300+
console.error('❌ Error:', error instanceof Error ? error.message : error);
301+
process.exit(1);
302+
}
303+
}
304+
305+
// Run the demo
306+
if (require.main === module) {
307+
main().catch(error => {
308+
console.error('❌ Failed:', error);
309+
process.exit(1);
310+
});
311+
}
312+
313+
export { pageToMarkdown, treeToMarkdown, buttonToMarkdown };

0 commit comments

Comments
 (0)