Skip to content

Commit f4c96f2

Browse files
ofriwclaude
andcommitted
Merge podcast branch: integrate line-breaking automation with validation
Hybrid approach combining: - Validation rules from main (source verification, fabrication prevention) - Line-breaking automation from podcast (deterministic 60-char wrapping) - Layout fixes from main (latest learning objectives styling) New files: - scripts/lib/line-breaker.js - Deterministic line-breaking module - scripts/fix-presentation-lines.js - Utility to fix existing presentations Modified: - scripts/generate-presentation.js - Added post-processing automation - scripts/lib/markdown-parser.js - Simplified (removed conditional blocks) - Visual components - Consistent formatting - Lesson markdown - Removed obsolete conditional markers Presentation JSONs kept from main - will be regenerated separately. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2 parents 6673726 + a079bb9 commit f4c96f2

File tree

4 files changed

+874
-35
lines changed

4 files changed

+874
-35
lines changed

scripts/fix-presentation-lines.js

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Fix Line Breaks in Existing Presentations
5+
*
6+
* Applies deterministic line breaking to existing presentation JSON files
7+
* to ensure all code blocks fit within the 60-character limit.
8+
*
9+
* Usage:
10+
* node scripts/fix-presentation-lines.js --all # Fix all presentations
11+
* node scripts/fix-presentation-lines.js --file lesson-4.json # Fix specific file
12+
* node scripts/fix-presentation-lines.js --dry-run --all # Show what would change
13+
*/
14+
15+
import { readFileSync, writeFileSync, readdirSync, existsSync, statSync } from 'fs';
16+
import { join, dirname, basename } from 'path';
17+
import { fileURLToPath } from 'url';
18+
import { processPresentation } from './lib/line-breaker.js';
19+
20+
// ES module __dirname equivalent
21+
const __filename = fileURLToPath(import.meta.url);
22+
const __dirname = dirname(__filename);
23+
24+
const PRESENTATIONS_DIR = join(__dirname, '../website/static/presentations');
25+
26+
/**
27+
* Parse command-line arguments
28+
*/
29+
function parseArgs() {
30+
const args = process.argv.slice(2);
31+
const config = {
32+
all: false,
33+
file: null,
34+
dryRun: false,
35+
};
36+
37+
for (let i = 0; i < args.length; i++) {
38+
if (args[i] === '--all') {
39+
config.all = true;
40+
} else if (args[i] === '--file' && i + 1 < args.length) {
41+
config.file = args[i + 1];
42+
i++;
43+
} else if (args[i] === '--dry-run') {
44+
config.dryRun = true;
45+
}
46+
}
47+
48+
return config;
49+
}
50+
51+
/**
52+
* Find all presentation JSON files (recursively)
53+
*/
54+
function findPresentationFiles(dir) {
55+
if (!existsSync(dir)) {
56+
console.error(`❌ Presentations directory not found: ${dir}`);
57+
return [];
58+
}
59+
60+
const files = [];
61+
62+
function traverse(currentDir) {
63+
const items = readdirSync(currentDir);
64+
65+
for (const item of items) {
66+
const fullPath = join(currentDir, item);
67+
const stat = statSync(fullPath);
68+
69+
if (stat.isDirectory()) {
70+
traverse(fullPath);
71+
} else if (item.endsWith('.json') && item !== 'manifest.json') {
72+
files.push(fullPath);
73+
}
74+
}
75+
}
76+
77+
traverse(dir);
78+
return files.sort();
79+
}
80+
81+
/**
82+
* Analyze a presentation for line length issues
83+
*/
84+
function analyzePresentation(presentation) {
85+
const issues = [];
86+
let maxLineLength = 0;
87+
let totalLongLines = 0;
88+
89+
for (const slide of presentation.slides || []) {
90+
// Check code slides
91+
if (slide.type === 'code' && slide.code) {
92+
const lines = slide.code.split('\n');
93+
for (let i = 0; i < lines.length; i++) {
94+
if (lines[i].length > 60) {
95+
totalLongLines++;
96+
maxLineLength = Math.max(maxLineLength, lines[i].length);
97+
issues.push({
98+
slideTitle: slide.title || 'Untitled',
99+
location: 'code',
100+
line: i + 1,
101+
length: lines[i].length,
102+
preview: lines[i].substring(0, 50) + '...',
103+
});
104+
}
105+
}
106+
}
107+
108+
// Check codeComparison slides
109+
if (slide.type === 'codeComparison') {
110+
if (slide.leftCode?.code) {
111+
const lines = slide.leftCode.code.split('\n');
112+
for (let i = 0; i < lines.length; i++) {
113+
if (lines[i].length > 60) {
114+
totalLongLines++;
115+
maxLineLength = Math.max(maxLineLength, lines[i].length);
116+
issues.push({
117+
slideTitle: slide.title || 'Untitled',
118+
location: 'leftCode',
119+
line: i + 1,
120+
length: lines[i].length,
121+
preview: lines[i].substring(0, 50) + '...',
122+
});
123+
}
124+
}
125+
}
126+
if (slide.rightCode?.code) {
127+
const lines = slide.rightCode.code.split('\n');
128+
for (let i = 0; i < lines.length; i++) {
129+
if (lines[i].length > 60) {
130+
totalLongLines++;
131+
maxLineLength = Math.max(maxLineLength, lines[i].length);
132+
issues.push({
133+
slideTitle: slide.title || 'Untitled',
134+
location: 'rightCode',
135+
line: i + 1,
136+
length: lines[i].length,
137+
preview: lines[i].substring(0, 50) + '...',
138+
});
139+
}
140+
}
141+
}
142+
}
143+
144+
// Check codeExecution slides
145+
if (slide.type === 'codeExecution' && Array.isArray(slide.steps)) {
146+
for (let i = 0; i < slide.steps.length; i++) {
147+
if (slide.steps[i].line && typeof slide.steps[i].line === 'string') {
148+
// Split by newlines and check each line individually
149+
const lines = slide.steps[i].line.split('\n');
150+
for (let j = 0; j < lines.length; j++) {
151+
if (lines[j].length > 60) {
152+
totalLongLines++;
153+
maxLineLength = Math.max(maxLineLength, lines[j].length);
154+
issues.push({
155+
slideTitle: slide.title || 'Untitled',
156+
location: `step ${i + 1}, line ${j + 1}`,
157+
line: j + 1,
158+
length: lines[j].length,
159+
preview: lines[j].substring(0, 50) + '...',
160+
});
161+
}
162+
}
163+
}
164+
}
165+
}
166+
}
167+
168+
return {
169+
totalLongLines,
170+
maxLineLength,
171+
issues: issues.slice(0, 5), // Show first 5 issues
172+
hasIssues: totalLongLines > 0,
173+
};
174+
}
175+
176+
/**
177+
* Process a single presentation file
178+
*/
179+
function processFile(filePath, dryRun = false) {
180+
const fileName = basename(filePath);
181+
console.log(`\n📄 ${fileName}`);
182+
183+
try {
184+
// Read and parse JSON
185+
const content = readFileSync(filePath, 'utf-8');
186+
const presentation = JSON.parse(content);
187+
188+
// Analyze before processing
189+
const beforeAnalysis = analyzePresentation(presentation);
190+
191+
if (!beforeAnalysis.hasIssues) {
192+
console.log(' ✅ No lines exceed 60 characters');
193+
return {
194+
success: true,
195+
changed: false,
196+
fileName,
197+
};
198+
}
199+
200+
console.log(` ⚠️ Found ${beforeAnalysis.totalLongLines} long lines (max: ${beforeAnalysis.maxLineLength} chars)`);
201+
202+
if (beforeAnalysis.issues.length > 0) {
203+
console.log(' 📍 Sample issues:');
204+
for (const issue of beforeAnalysis.issues) {
205+
console.log(` - "${issue.slideTitle}" (${issue.location}): ${issue.length} chars`);
206+
}
207+
}
208+
209+
// Apply line breaking
210+
const { presentation: processedPresentation, stats } = processPresentation(presentation);
211+
212+
// Analyze after processing
213+
const afterAnalysis = analyzePresentation(processedPresentation);
214+
215+
if (dryRun) {
216+
console.log(` 🔍 DRY RUN: Would fix ${stats.linesShortened} lines (max reduction: ${stats.maxReduction} chars)`);
217+
if (afterAnalysis.hasIssues) {
218+
console.log(` ⚠️ ${afterAnalysis.totalLongLines} lines would still exceed limit`);
219+
} else {
220+
console.log(' ✅ All lines would be within limit after processing');
221+
}
222+
} else {
223+
// Write back
224+
writeFileSync(filePath, JSON.stringify(processedPresentation, null, 2), 'utf-8');
225+
console.log(` ✂️ Fixed ${stats.linesShortened} lines (max reduction: ${stats.maxReduction} chars)`);
226+
227+
if (afterAnalysis.hasIssues) {
228+
console.log(` ⚠️ Warning: ${afterAnalysis.totalLongLines} lines still exceed limit`);
229+
} else {
230+
console.log(' ✅ All lines now within 60-character limit');
231+
}
232+
}
233+
234+
return {
235+
success: true,
236+
changed: stats.linesShortened > 0,
237+
fileName,
238+
stats,
239+
};
240+
} catch (error) {
241+
console.error(` ❌ Error: ${error.message}`);
242+
return {
243+
success: false,
244+
changed: false,
245+
fileName,
246+
error: error.message,
247+
};
248+
}
249+
}
250+
251+
/**
252+
* Main execution
253+
*/
254+
function main() {
255+
const config = parseArgs();
256+
257+
if (!config.all && !config.file) {
258+
console.log('Usage:');
259+
console.log(' node scripts/fix-presentation-lines.js --all');
260+
console.log(' node scripts/fix-presentation-lines.js --file lesson-4.json');
261+
console.log(' node scripts/fix-presentation-lines.js --dry-run --all');
262+
process.exit(1);
263+
}
264+
265+
console.log('🔧 Fixing Presentation Line Breaks');
266+
console.log('=' .repeat(50));
267+
268+
if (config.dryRun) {
269+
console.log('🔍 DRY RUN MODE - No files will be modified\n');
270+
}
271+
272+
let files = [];
273+
274+
if (config.all) {
275+
files = findPresentationFiles(PRESENTATIONS_DIR);
276+
console.log(`\nFound ${files.length} presentation files\n`);
277+
} else if (config.file) {
278+
let filePath;
279+
280+
// Handle absolute paths, relative paths from project root, or just filename
281+
if (config.file.startsWith('/')) {
282+
// Absolute path
283+
filePath = config.file;
284+
} else if (config.file.startsWith('website/')) {
285+
// Relative path from project root
286+
filePath = join(__dirname, '..', config.file);
287+
} else {
288+
// Just filename - look in PRESENTATIONS_DIR
289+
filePath = join(PRESENTATIONS_DIR, config.file);
290+
}
291+
292+
if (!existsSync(filePath)) {
293+
console.error(`❌ File not found: ${filePath}`);
294+
process.exit(1);
295+
}
296+
297+
files = [filePath];
298+
}
299+
300+
// Process all files
301+
const results = files.map(file => processFile(file, config.dryRun));
302+
303+
// Summary
304+
console.log('\n' + '='.repeat(50));
305+
console.log('📊 Summary\n');
306+
307+
const successful = results.filter(r => r.success);
308+
const changed = results.filter(r => r.changed);
309+
const errors = results.filter(r => !r.success);
310+
311+
console.log(`Total files processed: ${results.length}`);
312+
console.log(`✅ Successful: ${successful.length}`);
313+
console.log(`✂️ Modified: ${changed.length}`);
314+
315+
if (errors.length > 0) {
316+
console.log(`❌ Errors: ${errors.length}`);
317+
console.log('\nFailed files:');
318+
for (const result of errors) {
319+
console.log(` - ${result.fileName}: ${result.error}`);
320+
}
321+
}
322+
323+
if (changed.length > 0) {
324+
const totalLinesFixed = changed.reduce((sum, r) => sum + (r.stats?.linesShortened || 0), 0);
325+
const maxReduction = Math.max(...changed.map(r => r.stats?.maxReduction || 0));
326+
console.log(`\nTotal lines fixed: ${totalLinesFixed}`);
327+
console.log(`Maximum reduction: ${maxReduction} characters`);
328+
}
329+
330+
if (config.dryRun) {
331+
console.log('\n💡 Run without --dry-run to apply changes');
332+
}
333+
334+
console.log();
335+
process.exit(errors.length > 0 ? 1 : 0);
336+
}
337+
338+
main();

0 commit comments

Comments
 (0)