fix(worksheets): ten-frames not rendering in mastery mode

Fixed two critical bugs preventing ten-frames from rendering:

1. **Mastery mode not handled** (typstGenerator.ts:61)
   - Code only checked for 'smart' | 'manual' modes
   - Mastery mode fell into manual path, tried to use boolean flags that don't exist
   - Resulted in all display options being `undefined`
   - Fix: Check for both 'smart' OR 'mastery' modes (both use displayRules)

2. **Typst array membership syntax** (already fixed in previous commit)
   - Used `(i in array)` which doesn't work in Typst
   - Changed to `array.contains(i)`

Added comprehensive unit tests (tenFrames.test.ts):
- Problem analysis tests (regrouping detection)
- Display rule evaluation tests
- Full Typst template generation tests
- Mastery mode specific tests
- All 14 tests now passing

Added debug logging to trace display rules resolution:
- displayRules.ts: Shows rule evaluation per problem
- typstGenerator.ts: Shows enriched problems and Typst data
- Helps diagnose future issues

The issue was that mastery mode (which uses displayRules like smart mode)
was being treated as manual mode (which uses boolean flags), resulting in
undefined display options.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-11-10 10:06:18 -06:00
parent b08f9b8985
commit b36df3a40c
599 changed files with 65379 additions and 72463 deletions

View File

@@ -5,34 +5,29 @@
* This script captures git commit, branch, timestamp, and other metadata
*/
const { execSync } = require("child_process");
const fs = require("fs");
const path = require("path");
const { execSync } = require('child_process')
const fs = require('fs')
const path = require('path')
function exec(command) {
try {
return execSync(command, { encoding: "utf-8" }).trim();
return execSync(command, { encoding: 'utf-8' }).trim()
} catch (_error) {
return null;
return null
}
}
function getBuildInfo() {
// Try to get git info from environment variables first (for Docker builds)
// Fall back to git commands (for local development)
const gitCommit = process.env.GIT_COMMIT || exec("git rev-parse HEAD");
const gitCommitShort =
process.env.GIT_COMMIT_SHORT || exec("git rev-parse --short HEAD");
const gitBranch =
process.env.GIT_BRANCH || exec("git rev-parse --abbrev-ref HEAD");
const gitTag =
process.env.GIT_TAG ||
exec("git describe --tags --exact-match 2>/dev/null");
const gitCommit = process.env.GIT_COMMIT || exec('git rev-parse HEAD')
const gitCommitShort = process.env.GIT_COMMIT_SHORT || exec('git rev-parse --short HEAD')
const gitBranch = process.env.GIT_BRANCH || exec('git rev-parse --abbrev-ref HEAD')
const gitTag = process.env.GIT_TAG || exec('git describe --tags --exact-match 2>/dev/null')
const gitDirty =
process.env.GIT_DIRTY === "true" ||
exec('git diff --quiet || echo "dirty"') === "dirty";
process.env.GIT_DIRTY === 'true' || exec('git diff --quiet || echo "dirty"') === 'dirty'
const packageJson = require("../package.json");
const packageJson = require('../package.json')
return {
version: packageJson.version,
@@ -45,28 +40,22 @@ function getBuildInfo() {
tag: gitTag,
isDirty: gitDirty,
},
environment: process.env.NODE_ENV || "development",
environment: process.env.NODE_ENV || 'development',
buildNumber: process.env.BUILD_NUMBER || null,
nodeVersion: process.version,
};
}
}
const buildInfo = getBuildInfo();
const outputPath = path.join(
__dirname,
"..",
"src",
"generated",
"build-info.json",
);
const buildInfo = getBuildInfo()
const outputPath = path.join(__dirname, '..', 'src', 'generated', 'build-info.json')
// Ensure directory exists
const dir = path.dirname(outputPath);
const dir = path.dirname(outputPath)
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
fs.mkdirSync(dir, { recursive: true })
}
fs.writeFileSync(outputPath, JSON.stringify(buildInfo, null, 2));
fs.writeFileSync(outputPath, JSON.stringify(buildInfo, null, 2))
console.log("✅ Build info generated:", outputPath);
console.log(JSON.stringify(buildInfo, null, 2));
console.log('✅ Build info generated:', outputPath)
console.log(JSON.stringify(buildInfo, null, 2))

View File

@@ -7,20 +7,20 @@
* SVG output as the interactive client-side version (without animations).
*/
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { writeFileSync } from "fs";
import { join } from "path";
import { AbacusReact } from "@soroban/abacus-react";
import React from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { writeFileSync } from 'fs'
import { join } from 'path'
import { AbacusReact } from '@soroban/abacus-react'
// Extract just the SVG element content from rendered output
function extractSvgContent(markup: string): string {
// Find the opening <svg and closing </svg> tags
const svgMatch = markup.match(/<svg[^>]*>([\s\S]*?)<\/svg>/);
const svgMatch = markup.match(/<svg[^>]*>([\s\S]*?)<\/svg>/)
if (!svgMatch) {
throw new Error("No SVG element found in rendered output");
throw new Error('No SVG element found in rendered output')
}
return svgMatch[1]; // Return just the inner content
return svgMatch[1] // Return just the inner content
}
// Generate the favicon (icon.svg) - single column showing value 5
@@ -34,27 +34,27 @@ function generateFavicon(): string {
interactive={false}
showNumbers={false}
customStyles={{
heavenBeads: { fill: "#7c2d12", stroke: "#451a03", strokeWidth: 1 },
earthBeads: { fill: "#7c2d12", stroke: "#451a03", strokeWidth: 1 },
heavenBeads: { fill: '#7c2d12', stroke: '#451a03', strokeWidth: 1 },
earthBeads: { fill: '#7c2d12', stroke: '#451a03', strokeWidth: 1 },
columnPosts: {
fill: "#451a03",
stroke: "#292524",
fill: '#451a03',
stroke: '#292524',
strokeWidth: 2,
},
reckoningBar: {
fill: "#292524",
stroke: "#292524",
fill: '#292524',
stroke: '#292524',
strokeWidth: 3,
},
}}
/>,
);
/>
)
// Extract just the SVG content (without div wrapper)
let svgContent = extractSvgContent(abacusMarkup);
let svgContent = extractSvgContent(abacusMarkup)
// Remove !important from CSS (production code policy)
svgContent = svgContent.replace(/\s*!important/g, "");
svgContent = svgContent.replace(/\s*!important/g, '')
// Wrap in SVG with proper viewBox for favicon sizing
// AbacusReact with 1 column + scaleFactor 1.0 = ~25×120px
@@ -68,7 +68,7 @@ function generateFavicon(): string {
${svgContent}
</g>
</svg>
`;
`
}
// Generate the Open Graph image (og-image.svg)
@@ -83,46 +83,46 @@ function generateOGImage(): string {
showNumbers={false}
customStyles={{
columnPosts: {
fill: "rgb(255, 255, 255)",
stroke: "rgb(200, 200, 200)",
fill: 'rgb(255, 255, 255)',
stroke: 'rgb(200, 200, 200)',
strokeWidth: 2,
},
reckoningBar: {
fill: "rgb(255, 255, 255)",
stroke: "rgb(200, 200, 200)",
fill: 'rgb(255, 255, 255)',
stroke: 'rgb(200, 200, 200)',
strokeWidth: 3,
},
columns: {
0: {
// Ones place (rightmost) - Blue
heavenBeads: { fill: "#60a5fa", stroke: "#3b82f6", strokeWidth: 1 },
earthBeads: { fill: "#60a5fa", stroke: "#3b82f6", strokeWidth: 1 },
heavenBeads: { fill: '#60a5fa', stroke: '#3b82f6', strokeWidth: 1 },
earthBeads: { fill: '#60a5fa', stroke: '#3b82f6', strokeWidth: 1 },
},
1: {
// Tens place - Green
heavenBeads: { fill: "#4ade80", stroke: "#22c55e", strokeWidth: 1 },
earthBeads: { fill: "#4ade80", stroke: "#22c55e", strokeWidth: 1 },
heavenBeads: { fill: '#4ade80', stroke: '#22c55e', strokeWidth: 1 },
earthBeads: { fill: '#4ade80', stroke: '#22c55e', strokeWidth: 1 },
},
2: {
// Hundreds place - Yellow/Gold
heavenBeads: { fill: "#fbbf24", stroke: "#f59e0b", strokeWidth: 1 },
earthBeads: { fill: "#fbbf24", stroke: "#f59e0b", strokeWidth: 1 },
heavenBeads: { fill: '#fbbf24', stroke: '#f59e0b', strokeWidth: 1 },
earthBeads: { fill: '#fbbf24', stroke: '#f59e0b', strokeWidth: 1 },
},
3: {
// Thousands place (leftmost) - Purple
heavenBeads: { fill: "#c084fc", stroke: "#a855f7", strokeWidth: 1 },
earthBeads: { fill: "#c084fc", stroke: "#a855f7", strokeWidth: 1 },
heavenBeads: { fill: '#c084fc', stroke: '#a855f7', strokeWidth: 1 },
earthBeads: { fill: '#c084fc', stroke: '#a855f7', strokeWidth: 1 },
},
},
}}
/>,
);
/>
)
// Extract just the SVG content (without div wrapper)
let svgContent = extractSvgContent(abacusMarkup);
let svgContent = extractSvgContent(abacusMarkup)
// Remove !important from CSS (production code policy)
svgContent = svgContent.replace(/\s*!important/g, "");
svgContent = svgContent.replace(/\s*!important/g, '')
return `<svg width="1200" height="630" viewBox="0 0 1200 630" xmlns="http://www.w3.org/2000/svg">
<!-- Dark background like homepage -->
@@ -199,24 +199,22 @@ function generateOGImage(): string {
</linearGradient>
</defs>
</svg>
`;
`
}
// Main execution
const appDir = __dirname.replace("/scripts", "");
const appDir = __dirname.replace('/scripts', '')
try {
console.log("Generating Open Graph image from AbacusReact...");
const ogImageSvg = generateOGImage();
writeFileSync(join(appDir, "public", "og-image.svg"), ogImageSvg);
console.log("✓ Generated public/og-image.svg");
console.log('Generating Open Graph image from AbacusReact...')
const ogImageSvg = generateOGImage()
writeFileSync(join(appDir, 'public', 'og-image.svg'), ogImageSvg)
console.log('✓ Generated public/og-image.svg')
console.log("\n✅ Icon generated successfully!");
console.log(
"\nNote: Day-of-month favicons are generated on-demand by src/app/icon/route.tsx",
);
console.log("which calls scripts/generateDayIcon.tsx as a subprocess.");
console.log('\n✅ Icon generated successfully!')
console.log('\nNote: Day-of-month favicons are generated on-demand by src/app/icon/route.tsx')
console.log('which calls scripts/generateDayIcon.tsx as a subprocess.')
} catch (error) {
console.error("❌ Error generating icons:", error);
process.exit(1);
console.error('❌ Error generating icons:', error)
process.exit(1)
}

View File

@@ -8,19 +8,19 @@
*/
// biome-ignore lint/correctness/noUnusedImports: React is required for JSX transform
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { writeFileSync, mkdirSync } from "fs";
import { join } from "path";
import { AbacusStatic } from "@soroban/abacus-react";
import React from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { writeFileSync, mkdirSync } from 'fs'
import { join } from 'path'
import { AbacusStatic } from '@soroban/abacus-react'
// Extract just the SVG element from rendered output
function extractSvgElement(markup: string): string {
const svgMatch = markup.match(/<svg[^>]*>[\s\S]*?<\/svg>/);
const svgMatch = markup.match(/<svg[^>]*>[\s\S]*?<\/svg>/)
if (!svgMatch) {
throw new Error("No SVG element found in rendered output");
throw new Error('No SVG element found in rendered output')
}
return svgMatch[0];
return svgMatch[0]
}
// Generate a single day icon
@@ -44,97 +44,95 @@ function generateDayIcon(day: number): string {
}}
customStyles={{
columnPosts: {
fill: "#1c1917",
stroke: "#0c0a09",
fill: '#1c1917',
stroke: '#0c0a09',
strokeWidth: 2,
},
reckoningBar: {
fill: "#1c1917",
stroke: "#0c0a09",
fill: '#1c1917',
stroke: '#0c0a09',
strokeWidth: 3,
},
columns: {
0: {
// Ones place - Gold (royal theme)
heavenBeads: { fill: "#fbbf24", stroke: "#f59e0b", strokeWidth: 2 },
earthBeads: { fill: "#fbbf24", stroke: "#f59e0b", strokeWidth: 2 },
heavenBeads: { fill: '#fbbf24', stroke: '#f59e0b', strokeWidth: 2 },
earthBeads: { fill: '#fbbf24', stroke: '#f59e0b', strokeWidth: 2 },
},
1: {
// Tens place - Purple (royal theme)
heavenBeads: { fill: "#a855f7", stroke: "#7e22ce", strokeWidth: 2 },
earthBeads: { fill: "#a855f7", stroke: "#7e22ce", strokeWidth: 2 },
heavenBeads: { fill: '#a855f7', stroke: '#7e22ce', strokeWidth: 2 },
earthBeads: { fill: '#a855f7', stroke: '#7e22ce', strokeWidth: 2 },
},
},
}}
/>,
);
/>
)
// Extract the cropped SVG
let croppedSvg = extractSvgElement(abacusMarkup);
let croppedSvg = extractSvgElement(abacusMarkup)
// Remove !important from CSS (production code policy)
croppedSvg = croppedSvg.replace(/\s*!important/g, "");
croppedSvg = croppedSvg.replace(/\s*!important/g, '')
// Parse width and height from the cropped SVG
const widthMatch = croppedSvg.match(/width="([^"]+)"/);
const heightMatch = croppedSvg.match(/height="([^"]+)"/);
const widthMatch = croppedSvg.match(/width="([^"]+)"/)
const heightMatch = croppedSvg.match(/height="([^"]+)"/)
if (!widthMatch || !heightMatch) {
throw new Error("Could not parse dimensions from cropped SVG");
throw new Error('Could not parse dimensions from cropped SVG')
}
const croppedWidth = parseFloat(widthMatch[1]);
const croppedHeight = parseFloat(heightMatch[1]);
const croppedWidth = parseFloat(widthMatch[1])
const croppedHeight = parseFloat(heightMatch[1])
// Calculate scale to fit cropped region into 96x96 (leaving room for border)
const targetSize = 96;
const scale = Math.min(targetSize / croppedWidth, targetSize / croppedHeight);
const targetSize = 96
const scale = Math.min(targetSize / croppedWidth, targetSize / croppedHeight)
// Center in 100x100 canvas
const scaledWidth = croppedWidth * scale;
const scaledHeight = croppedHeight * scale;
const offsetX = (100 - scaledWidth) / 2;
const offsetY = (100 - scaledHeight) / 2;
const scaledWidth = croppedWidth * scale
const scaledHeight = croppedHeight * scale
const offsetX = (100 - scaledWidth) / 2
const offsetY = (100 - scaledHeight) / 2
// Wrap in 100x100 SVG canvas for favicon
// Extract viewBox from cropped SVG to preserve it
const viewBoxMatch = croppedSvg.match(/viewBox="([^"]+)"/);
const viewBox = viewBoxMatch
? viewBoxMatch[1]
: `0 0 ${croppedWidth} ${croppedHeight}`;
const viewBoxMatch = croppedSvg.match(/viewBox="([^"]+)"/)
const viewBox = viewBoxMatch ? viewBoxMatch[1] : `0 0 ${croppedWidth} ${croppedHeight}`
return `<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Abacus showing day ${day.toString().padStart(2, "0")} (US Central Time) - cropped to active beads -->
<!-- Abacus showing day ${day.toString().padStart(2, '0')} (US Central Time) - cropped to active beads -->
<svg x="${offsetX}" y="${offsetY}" width="${scaledWidth}" height="${scaledHeight}"
viewBox="${viewBox}">
${croppedSvg.match(/<svg[^>]*>([\s\S]*?)<\/svg>/)?.[1] || ""}
${croppedSvg.match(/<svg[^>]*>([\s\S]*?)<\/svg>/)?.[1] || ''}
</svg>
</svg>
`;
`
}
// Main execution
const publicDir = join(__dirname, "..", "public");
const iconsDir = join(publicDir, "icons");
const publicDir = join(__dirname, '..', 'public')
const iconsDir = join(publicDir, 'icons')
try {
// Ensure icons directory exists
mkdirSync(iconsDir, { recursive: true });
mkdirSync(iconsDir, { recursive: true })
console.log("Generating all 31 day-of-month favicons...\n");
console.log('Generating all 31 day-of-month favicons...\n')
// Generate all 31 days
for (let day = 1; day <= 31; day++) {
const svg = generateDayIcon(day);
const filename = `icon-day-${day.toString().padStart(2, "0")}.svg`;
const filepath = join(iconsDir, filename);
writeFileSync(filepath, svg);
console.log(`✓ Generated ${filename}`);
const svg = generateDayIcon(day)
const filename = `icon-day-${day.toString().padStart(2, '0')}.svg`
const filepath = join(iconsDir, filename)
writeFileSync(filepath, svg)
console.log(`✓ Generated ${filename}`)
}
console.log("\n✅ All day icons generated successfully!");
console.log(` Location: public/icons/icon-day-*.svg`);
console.log('\n✅ All day icons generated successfully!')
console.log(` Location: public/icons/icon-day-*.svg`)
} catch (error) {
console.error("❌ Error generating day icons:", error);
process.exit(1);
console.error('❌ Error generating day icons:', error)
process.exit(1)
}

View File

@@ -1,102 +1,97 @@
// Script to generate example worksheet images for the blog post
// Shows different scaffolding levels for the 2D difficulty blog post
import fs from "fs";
import path from "path";
import { generateWorksheetPreview } from "../src/app/create/worksheets/addition/generatePreview";
import { DIFFICULTY_PROFILES } from "../src/app/create/worksheets/addition/difficultyProfiles";
import fs from 'fs'
import path from 'path'
import { generateWorksheetPreview } from '../src/app/create/worksheets/addition/generatePreview'
import { DIFFICULTY_PROFILES } from '../src/app/create/worksheets/addition/difficultyProfiles'
// Output directory
const outputDir = path.join(
process.cwd(),
"public",
"blog",
"difficulty-examples",
);
const outputDir = path.join(process.cwd(), 'public', 'blog', 'difficulty-examples')
// Ensure output directory exists
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
fs.mkdirSync(outputDir, { recursive: true })
}
// Generate examples with SAME regrouping level but different scaffolding
// This clearly shows how scaffolding changes while keeping problem complexity constant
const examples = [
{
name: "full-scaffolding",
filename: "full-scaffolding.svg",
description: "Full Scaffolding: Maximum visual support",
name: 'full-scaffolding',
filename: 'full-scaffolding.svg',
description: 'Full Scaffolding: Maximum visual support',
// Use medium regrouping with full scaffolding
config: {
pAllStart: 0.3,
pAnyStart: 0.7,
displayRules: {
carryBoxes: "always" as const,
answerBoxes: "always" as const,
placeValueColors: "always" as const,
tenFrames: "always" as const,
problemNumbers: "always" as const,
cellBorders: "always" as const,
carryBoxes: 'always' as const,
answerBoxes: 'always' as const,
placeValueColors: 'always' as const,
tenFrames: 'always' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
},
},
},
{
name: "medium-scaffolding",
filename: "medium-scaffolding.svg",
description: "Medium Scaffolding: Strategic support",
name: 'medium-scaffolding',
filename: 'medium-scaffolding.svg',
description: 'Medium Scaffolding: Strategic support',
config: {
pAllStart: 0.3,
pAnyStart: 0.7,
displayRules: {
carryBoxes: "whenRegrouping" as const,
answerBoxes: "always" as const,
placeValueColors: "when3PlusDigits" as const,
tenFrames: "never" as const,
problemNumbers: "always" as const,
cellBorders: "always" as const,
carryBoxes: 'whenRegrouping' as const,
answerBoxes: 'always' as const,
placeValueColors: 'when3PlusDigits' as const,
tenFrames: 'never' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
},
},
},
{
name: "minimal-scaffolding",
filename: "minimal-scaffolding.svg",
description: "Minimal Scaffolding: Carry boxes only",
name: 'minimal-scaffolding',
filename: 'minimal-scaffolding.svg',
description: 'Minimal Scaffolding: Carry boxes only',
config: {
pAllStart: 0.3,
pAnyStart: 0.7,
displayRules: {
carryBoxes: "whenMultipleRegroups" as const,
answerBoxes: "never" as const,
placeValueColors: "never" as const,
tenFrames: "never" as const,
problemNumbers: "always" as const,
cellBorders: "always" as const,
carryBoxes: 'whenMultipleRegroups' as const,
answerBoxes: 'never' as const,
placeValueColors: 'never' as const,
tenFrames: 'never' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
},
},
},
{
name: "no-scaffolding",
filename: "no-scaffolding.svg",
description: "No Scaffolding: Students work independently",
name: 'no-scaffolding',
filename: 'no-scaffolding.svg',
description: 'No Scaffolding: Students work independently',
config: {
pAllStart: 0.3,
pAnyStart: 0.7,
displayRules: {
carryBoxes: "never" as const,
answerBoxes: "never" as const,
placeValueColors: "never" as const,
tenFrames: "never" as const,
problemNumbers: "always" as const,
cellBorders: "always" as const,
carryBoxes: 'never' as const,
answerBoxes: 'never' as const,
placeValueColors: 'never' as const,
tenFrames: 'never' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
},
},
},
] as const;
] as const
console.log("Generating blog example worksheets...\n");
console.log('Generating blog example worksheets...\n')
for (const example of examples) {
console.log(`Generating ${example.description}...`);
console.log(`Generating ${example.description}...`)
const config = {
pAllStart: example.config.pAllStart,
@@ -105,28 +100,28 @@ for (const example of examples) {
problemsPerPage: 4,
pages: 1,
cols: 2,
};
}
try {
const result = generateWorksheetPreview(config);
const result = generateWorksheetPreview(config)
if (!result.success || !result.pages || result.pages.length === 0) {
console.error(`Failed to generate ${example.name}:`, result.error);
continue;
console.error(`Failed to generate ${example.name}:`, result.error)
continue
}
// Get the first page's SVG
const svg = result.pages[0];
const svg = result.pages[0]
// Save to file
const outputPath = path.join(outputDir, example.filename);
fs.writeFileSync(outputPath, svg, "utf-8");
const outputPath = path.join(outputDir, example.filename)
fs.writeFileSync(outputPath, svg, 'utf-8')
console.log(` ✓ Saved to ${outputPath}`);
console.log(` ✓ Saved to ${outputPath}`)
} catch (error) {
console.error(` ✗ Error generating ${example.name}:`, error);
console.error(` ✗ Error generating ${example.name}:`, error)
}
}
console.log("\nDone! Example worksheets generated.");
console.log(`\nFiles saved to: ${outputDir}`);
console.log('\nDone! Example worksheets generated.')
console.log(`\nFiles saved to: ${outputDir}`)

View File

@@ -6,26 +6,26 @@
* Example: npx tsx scripts/generateDayIcon.tsx 15
*/
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { AbacusStatic } from "@soroban/abacus-react";
import React from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { AbacusStatic } from '@soroban/abacus-react'
// Extract just the SVG element from rendered output
function extractSvgElement(markup: string): string {
const svgMatch = markup.match(/<svg[^>]*>[\s\S]*?<\/svg>/);
const svgMatch = markup.match(/<svg[^>]*>[\s\S]*?<\/svg>/)
if (!svgMatch) {
throw new Error("No SVG element found in rendered output");
throw new Error('No SVG element found in rendered output')
}
return svgMatch[0];
return svgMatch[0]
}
// Get day from command line argument
const day = parseInt(process.argv[2], 10);
const day = parseInt(process.argv[2], 10)
if (!day || day < 1 || day > 31) {
console.error("Usage: npx tsx scripts/generateDayIcon.tsx <day>");
console.error("Example: npx tsx scripts/generateDayIcon.tsx 15");
process.exit(1);
console.error('Usage: npx tsx scripts/generateDayIcon.tsx <day>')
console.error('Example: npx tsx scripts/generateDayIcon.tsx 15')
process.exit(1)
}
// Render 2-column abacus showing day of month
@@ -48,73 +48,71 @@ const abacusMarkup = renderToStaticMarkup(
}}
customStyles={{
columnPosts: {
fill: "#1c1917",
stroke: "#0c0a09",
fill: '#1c1917',
stroke: '#0c0a09',
strokeWidth: 2,
},
reckoningBar: {
fill: "#1c1917",
stroke: "#0c0a09",
fill: '#1c1917',
stroke: '#0c0a09',
strokeWidth: 3,
},
columns: {
0: {
// Ones place - Gold (royal theme)
heavenBeads: { fill: "#fbbf24", stroke: "#f59e0b", strokeWidth: 2 },
earthBeads: { fill: "#fbbf24", stroke: "#f59e0b", strokeWidth: 2 },
heavenBeads: { fill: '#fbbf24', stroke: '#f59e0b', strokeWidth: 2 },
earthBeads: { fill: '#fbbf24', stroke: '#f59e0b', strokeWidth: 2 },
},
1: {
// Tens place - Purple (royal theme)
heavenBeads: { fill: "#a855f7", stroke: "#7e22ce", strokeWidth: 2 },
earthBeads: { fill: "#a855f7", stroke: "#7e22ce", strokeWidth: 2 },
heavenBeads: { fill: '#a855f7', stroke: '#7e22ce', strokeWidth: 2 },
earthBeads: { fill: '#a855f7', stroke: '#7e22ce', strokeWidth: 2 },
},
},
}}
/>,
);
/>
)
// Extract the cropped SVG
let croppedSvg = extractSvgElement(abacusMarkup);
let croppedSvg = extractSvgElement(abacusMarkup)
// Remove !important from CSS (production code policy)
croppedSvg = croppedSvg.replace(/\s*!important/g, "");
croppedSvg = croppedSvg.replace(/\s*!important/g, '')
// Parse width and height from the cropped SVG
const widthMatch = croppedSvg.match(/width="([^"]+)"/);
const heightMatch = croppedSvg.match(/height="([^"]+)"/);
const widthMatch = croppedSvg.match(/width="([^"]+)"/)
const heightMatch = croppedSvg.match(/height="([^"]+)"/)
if (!widthMatch || !heightMatch) {
throw new Error("Could not parse dimensions from cropped SVG");
throw new Error('Could not parse dimensions from cropped SVG')
}
const croppedWidth = parseFloat(widthMatch[1]);
const croppedHeight = parseFloat(heightMatch[1]);
const croppedWidth = parseFloat(widthMatch[1])
const croppedHeight = parseFloat(heightMatch[1])
// Calculate scale to fit cropped region into 96x96 (leaving room for border)
const targetSize = 96;
const scale = Math.min(targetSize / croppedWidth, targetSize / croppedHeight);
const targetSize = 96
const scale = Math.min(targetSize / croppedWidth, targetSize / croppedHeight)
// Center in 100x100 canvas
const scaledWidth = croppedWidth * scale;
const scaledHeight = croppedHeight * scale;
const offsetX = (100 - scaledWidth) / 2;
const offsetY = (100 - scaledHeight) / 2;
const scaledWidth = croppedWidth * scale
const scaledHeight = croppedHeight * scale
const offsetX = (100 - scaledWidth) / 2
const offsetY = (100 - scaledHeight) / 2
// Wrap in 100x100 SVG canvas for favicon
// Extract viewBox from cropped SVG to preserve it
const viewBoxMatch = croppedSvg.match(/viewBox="([^"]+)"/);
const viewBox = viewBoxMatch
? viewBoxMatch[1]
: `0 0 ${croppedWidth} ${croppedHeight}`;
const viewBoxMatch = croppedSvg.match(/viewBox="([^"]+)"/)
const viewBox = viewBoxMatch ? viewBoxMatch[1] : `0 0 ${croppedWidth} ${croppedHeight}`
const svg = `<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Abacus showing day ${day.toString().padStart(2, "0")} (US Central Time) - cropped to active beads -->
<!-- Abacus showing day ${day.toString().padStart(2, '0')} (US Central Time) - cropped to active beads -->
<svg x="${offsetX}" y="${offsetY}" width="${scaledWidth}" height="${scaledHeight}"
viewBox="${viewBox}">
${croppedSvg.match(/<svg[^>]*>([\s\S]*?)<\/svg>/)?.[1] || ""}
${croppedSvg.match(/<svg[^>]*>([\s\S]*?)<\/svg>/)?.[1] || ''}
</svg>
</svg>
`;
`
// Output to stdout so parent process can capture it
process.stdout.write(svg);
process.stdout.write(svg)

View File

@@ -1,42 +1,37 @@
// Script to generate multi-digit (>2 digits) worksheet examples for the blog post
// Shows how scaffolding adapts to different digit ranges
import fs from "fs";
import path from "path";
import { generateWorksheetPreview } from "../src/app/create/worksheets/addition/generatePreview";
import fs from 'fs'
import path from 'path'
import { generateWorksheetPreview } from '../src/app/create/worksheets/addition/generatePreview'
// Output directory
const outputDir = path.join(
process.cwd(),
"public",
"blog",
"multi-digit-examples",
);
const outputDir = path.join(process.cwd(), 'public', 'blog', 'multi-digit-examples')
// Ensure output directory exists
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
fs.mkdirSync(outputDir, { recursive: true })
}
// Generate examples showing different digit ranges and adaptive scaffolding
const examples = [
{
name: "two-digit-addition",
filename: "two-digit.svg",
description: "2-digit addition (baseline)",
name: 'two-digit-addition',
filename: 'two-digit.svg',
description: '2-digit addition (baseline)',
config: {
operator: "addition" as const,
operator: 'addition' as const,
pAllStart: 0.0,
pAnyStart: 0.5,
digitRange: { min: 2, max: 2 },
mode: "manual" as const,
mode: 'manual' as const,
displayRules: {
carryBoxes: "always" as const,
answerBoxes: "always" as const,
placeValueColors: "never" as const,
tenFrames: "never" as const,
problemNumbers: "always" as const,
cellBorders: "always" as const,
carryBoxes: 'always' as const,
answerBoxes: 'always' as const,
placeValueColors: 'never' as const,
tenFrames: 'never' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
},
showBorrowNotation: false,
showBorrowingHints: false,
@@ -44,22 +39,22 @@ const examples = [
},
},
{
name: "three-digit-with-colors",
filename: "three-digit-colors.svg",
description: "3-digit addition with place value colors",
name: 'three-digit-with-colors',
filename: 'three-digit-colors.svg',
description: '3-digit addition with place value colors',
config: {
operator: "addition" as const,
operator: 'addition' as const,
pAllStart: 0.0,
pAnyStart: 0.5,
digitRange: { min: 3, max: 3 },
mode: "manual" as const,
mode: 'manual' as const,
displayRules: {
carryBoxes: "always" as const,
answerBoxes: "always" as const,
placeValueColors: "always" as const,
tenFrames: "never" as const,
problemNumbers: "always" as const,
cellBorders: "always" as const,
carryBoxes: 'always' as const,
answerBoxes: 'always' as const,
placeValueColors: 'always' as const,
tenFrames: 'never' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
},
showBorrowNotation: false,
showBorrowingHints: false,
@@ -68,22 +63,22 @@ const examples = [
},
},
{
name: "four-digit-addition",
filename: "four-digit.svg",
description: "4-digit addition with adaptive scaffolding",
name: 'four-digit-addition',
filename: 'four-digit.svg',
description: '4-digit addition with adaptive scaffolding',
config: {
operator: "addition" as const,
operator: 'addition' as const,
pAllStart: 0.0,
pAnyStart: 0.6,
digitRange: { min: 4, max: 4 },
mode: "manual" as const,
mode: 'manual' as const,
displayRules: {
carryBoxes: "always" as const,
answerBoxes: "always" as const,
placeValueColors: "always" as const,
tenFrames: "never" as const,
problemNumbers: "always" as const,
cellBorders: "always" as const,
carryBoxes: 'always' as const,
answerBoxes: 'always' as const,
placeValueColors: 'always' as const,
tenFrames: 'never' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
},
showBorrowNotation: false,
showBorrowingHints: false,
@@ -92,22 +87,22 @@ const examples = [
},
},
{
name: "five-digit-addition",
filename: "five-digit.svg",
description: "5-digit addition (maximum complexity)",
name: 'five-digit-addition',
filename: 'five-digit.svg',
description: '5-digit addition (maximum complexity)',
config: {
operator: "addition" as const,
operator: 'addition' as const,
pAllStart: 0.3,
pAnyStart: 0.8,
digitRange: { min: 5, max: 5 },
mode: "manual" as const,
mode: 'manual' as const,
displayRules: {
carryBoxes: "always" as const,
answerBoxes: "always" as const,
placeValueColors: "always" as const,
tenFrames: "never" as const,
problemNumbers: "always" as const,
cellBorders: "always" as const,
carryBoxes: 'always' as const,
answerBoxes: 'always' as const,
placeValueColors: 'always' as const,
tenFrames: 'never' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
},
showBorrowNotation: false,
showBorrowingHints: false,
@@ -116,22 +111,22 @@ const examples = [
},
},
{
name: "mixed-digit-range",
filename: "mixed-range.svg",
description: "Mixed problem sizes (2-4 digits)",
name: 'mixed-digit-range',
filename: 'mixed-range.svg',
description: 'Mixed problem sizes (2-4 digits)',
config: {
operator: "addition" as const,
operator: 'addition' as const,
pAllStart: 0.0,
pAnyStart: 0.5,
digitRange: { min: 2, max: 4 },
mode: "manual" as const,
mode: 'manual' as const,
displayRules: {
carryBoxes: "always" as const,
answerBoxes: "always" as const,
placeValueColors: "always" as const,
tenFrames: "never" as const,
problemNumbers: "always" as const,
cellBorders: "always" as const,
carryBoxes: 'always' as const,
answerBoxes: 'always' as const,
placeValueColors: 'always' as const,
tenFrames: 'never' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
},
showBorrowNotation: false,
showBorrowingHints: false,
@@ -140,33 +135,33 @@ const examples = [
},
},
{
name: "three-digit-subtraction",
filename: "three-digit-subtraction.svg",
description: "3-digit subtraction with borrowing",
name: 'three-digit-subtraction',
filename: 'three-digit-subtraction.svg',
description: '3-digit subtraction with borrowing',
config: {
operator: "subtraction" as const,
operator: 'subtraction' as const,
pAllStart: 0.0,
pAnyStart: 0.8,
digitRange: { min: 3, max: 3 },
mode: "manual" as const,
mode: 'manual' as const,
displayRules: {
carryBoxes: "never" as const,
answerBoxes: "always" as const,
placeValueColors: "always" as const,
tenFrames: "never" as const,
problemNumbers: "always" as const,
cellBorders: "always" as const,
carryBoxes: 'never' as const,
answerBoxes: 'always' as const,
placeValueColors: 'always' as const,
tenFrames: 'never' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
},
showBorrowNotation: true,
showBorrowingHints: false,
},
},
] as const;
] as const
console.log("Generating multi-digit example worksheets...\n");
console.log('Generating multi-digit example worksheets...\n')
for (const example of examples) {
console.log(`Generating ${example.description}...`);
console.log(`Generating ${example.description}...`)
const config = {
...example.config,
@@ -174,29 +169,29 @@ for (const example of examples) {
pages: 1,
cols: 2,
seed: 54321, // Fixed seed for consistent examples
};
}
try {
const result = generateWorksheetPreview(config);
const result = generateWorksheetPreview(config)
if (!result.success || !result.pages || result.pages.length === 0) {
console.error(`Failed to generate ${example.name}:`, result.error);
console.error(`Details:`, result.details);
continue;
console.error(`Failed to generate ${example.name}:`, result.error)
console.error(`Details:`, result.details)
continue
}
// Get the first page's SVG
const svg = result.pages[0];
const svg = result.pages[0]
// Save to file
const outputPath = path.join(outputDir, example.filename);
fs.writeFileSync(outputPath, svg, "utf-8");
const outputPath = path.join(outputDir, example.filename)
fs.writeFileSync(outputPath, svg, 'utf-8')
console.log(` ✓ Saved to ${outputPath}`);
console.log(` ✓ Saved to ${outputPath}`)
} catch (error) {
console.error(` ✗ Error generating ${example.name}:`, error);
console.error(` ✗ Error generating ${example.name}:`, error)
}
}
console.log("\nDone! Multi-digit example worksheets generated.");
console.log(`\nFiles saved to: ${outputDir}`);
console.log('\nDone! Multi-digit example worksheets generated.')
console.log(`\nFiles saved to: ${outputDir}`)

View File

@@ -1,174 +1,169 @@
// Script to generate subtraction worksheet examples for the blog post
// Shows different scaffolding levels for subtraction problems
import fs from "fs";
import path from "path";
import { generateWorksheetPreview } from "../src/app/create/worksheets/addition/generatePreview";
import fs from 'fs'
import path from 'path'
import { generateWorksheetPreview } from '../src/app/create/worksheets/addition/generatePreview'
// Output directory
const outputDir = path.join(
process.cwd(),
"public",
"blog",
"subtraction-examples",
);
const outputDir = path.join(process.cwd(), 'public', 'blog', 'subtraction-examples')
// Ensure output directory exists
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
fs.mkdirSync(outputDir, { recursive: true })
}
// Generate examples showing different subtraction scaffolding options
const examples = [
{
name: "subtraction-no-borrowing",
filename: "no-borrowing.svg",
description: "Simple subtraction (no borrowing needed)",
name: 'subtraction-no-borrowing',
filename: 'no-borrowing.svg',
description: 'Simple subtraction (no borrowing needed)',
config: {
operator: "subtraction" as const,
operator: 'subtraction' as const,
pAllStart: 0.0, // No borrowing problems
pAnyStart: 0.0,
digitRange: { min: 2, max: 2 },
mode: "manual" as const,
mode: 'manual' as const,
displayRules: {
carryBoxes: "never" as const,
answerBoxes: "always" as const,
placeValueColors: "never" as const,
tenFrames: "never" as const,
problemNumbers: "always" as const,
cellBorders: "always" as const,
carryBoxes: 'never' as const,
answerBoxes: 'always' as const,
placeValueColors: 'never' as const,
tenFrames: 'never' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
},
showBorrowNotation: false,
showBorrowingHints: false,
},
},
{
name: "subtraction-with-borrow-notation",
filename: "with-borrow-notation.svg",
description: "Subtraction with borrow notation boxes",
name: 'subtraction-with-borrow-notation',
filename: 'with-borrow-notation.svg',
description: 'Subtraction with borrow notation boxes',
config: {
operator: "subtraction" as const,
operator: 'subtraction' as const,
pAllStart: 0.0,
pAnyStart: 1.0, // Some borrowing
digitRange: { min: 2, max: 2 },
mode: "manual" as const,
mode: 'manual' as const,
displayRules: {
carryBoxes: "never" as const,
answerBoxes: "always" as const,
placeValueColors: "always" as const,
tenFrames: "never" as const,
problemNumbers: "always" as const,
cellBorders: "always" as const,
carryBoxes: 'never' as const,
answerBoxes: 'always' as const,
placeValueColors: 'always' as const,
tenFrames: 'never' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
},
showBorrowNotation: true,
showBorrowingHints: false,
},
},
{
name: "subtraction-with-hints",
filename: "with-borrowing-hints.svg",
description: "Subtraction with borrow notation and hints",
name: 'subtraction-with-hints',
filename: 'with-borrowing-hints.svg',
description: 'Subtraction with borrow notation and hints',
config: {
operator: "subtraction" as const,
operator: 'subtraction' as const,
pAllStart: 0.0,
pAnyStart: 1.0, // Some borrowing
digitRange: { min: 2, max: 2 },
mode: "manual" as const,
mode: 'manual' as const,
displayRules: {
carryBoxes: "never" as const,
answerBoxes: "always" as const,
placeValueColors: "always" as const,
tenFrames: "never" as const,
problemNumbers: "always" as const,
cellBorders: "always" as const,
carryBoxes: 'never' as const,
answerBoxes: 'always' as const,
placeValueColors: 'always' as const,
tenFrames: 'never' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
},
showBorrowNotation: true,
showBorrowingHints: true,
},
},
{
name: "subtraction-multiple-borrows",
filename: "multiple-borrows.svg",
description: "Complex subtraction with multiple borrows",
name: 'subtraction-multiple-borrows',
filename: 'multiple-borrows.svg',
description: 'Complex subtraction with multiple borrows',
config: {
operator: "subtraction" as const,
operator: 'subtraction' as const,
pAllStart: 1.0, // All problems require borrowing
pAnyStart: 1.0,
digitRange: { min: 3, max: 3 },
mode: "manual" as const,
mode: 'manual' as const,
displayRules: {
carryBoxes: "never" as const,
answerBoxes: "always" as const,
placeValueColors: "always" as const,
tenFrames: "never" as const,
problemNumbers: "always" as const,
cellBorders: "always" as const,
carryBoxes: 'never' as const,
answerBoxes: 'always' as const,
placeValueColors: 'always' as const,
tenFrames: 'never' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
},
showBorrowNotation: true,
showBorrowingHints: false,
},
},
{
name: "subtraction-single-borrow",
filename: "single-borrow-ones.svg",
description: "Single borrow in ones place only",
name: 'subtraction-single-borrow',
filename: 'single-borrow-ones.svg',
description: 'Single borrow in ones place only',
config: {
operator: "subtraction" as const,
operator: 'subtraction' as const,
pAllStart: 0.0,
pAnyStart: 1.0, // Some borrowing
digitRange: { min: 2, max: 2 },
mode: "manual" as const,
mode: 'manual' as const,
displayRules: {
carryBoxes: "never" as const,
answerBoxes: "always" as const,
placeValueColors: "always" as const,
tenFrames: "never" as const,
problemNumbers: "always" as const,
cellBorders: "always" as const,
carryBoxes: 'never' as const,
answerBoxes: 'always' as const,
placeValueColors: 'always' as const,
tenFrames: 'never' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
},
showBorrowNotation: true,
showBorrowingHints: false,
},
},
{
name: "subtraction-comparison-no-notation",
filename: "comparison-no-notation.svg",
description: "Borrowing problems WITHOUT notation boxes",
name: 'subtraction-comparison-no-notation',
filename: 'comparison-no-notation.svg',
description: 'Borrowing problems WITHOUT notation boxes',
config: {
operator: "subtraction" as const,
operator: 'subtraction' as const,
pAllStart: 0.0,
pAnyStart: 1.0,
digitRange: { min: 2, max: 2 },
mode: "manual" as const,
mode: 'manual' as const,
displayRules: {
carryBoxes: "never" as const,
answerBoxes: "always" as const,
placeValueColors: "never" as const,
tenFrames: "never" as const,
problemNumbers: "always" as const,
cellBorders: "always" as const,
carryBoxes: 'never' as const,
answerBoxes: 'always' as const,
placeValueColors: 'never' as const,
tenFrames: 'never' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
},
showBorrowNotation: false,
showBorrowingHints: false,
},
},
{
name: "subtraction-comparison-with-notation",
filename: "comparison-with-notation.svg",
description: "Same problems WITH notation boxes",
name: 'subtraction-comparison-with-notation',
filename: 'comparison-with-notation.svg',
description: 'Same problems WITH notation boxes',
config: {
operator: "subtraction" as const,
operator: 'subtraction' as const,
pAllStart: 0.0,
pAnyStart: 1.0,
digitRange: { min: 2, max: 2 },
mode: "manual" as const,
mode: 'manual' as const,
displayRules: {
carryBoxes: "never" as const,
answerBoxes: "always" as const,
placeValueColors: "never" as const,
tenFrames: "never" as const,
problemNumbers: "always" as const,
cellBorders: "always" as const,
carryBoxes: 'never' as const,
answerBoxes: 'always' as const,
placeValueColors: 'never' as const,
tenFrames: 'never' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
},
showBorrowNotation: true,
showBorrowingHints: false,
@@ -176,44 +171,44 @@ const examples = [
},
},
{
name: "subtraction-cascading-borrows",
filename: "cascading-borrows.svg",
description: "Cascading borrows across multiple places",
name: 'subtraction-cascading-borrows',
filename: 'cascading-borrows.svg',
description: 'Cascading borrows across multiple places',
config: {
operator: "subtraction" as const,
operator: 'subtraction' as const,
pAllStart: 1.0,
pAnyStart: 1.0,
digitRange: { min: 4, max: 4 },
mode: "manual" as const,
mode: 'manual' as const,
displayRules: {
carryBoxes: "never" as const,
answerBoxes: "always" as const,
placeValueColors: "always" as const,
tenFrames: "never" as const,
problemNumbers: "always" as const,
cellBorders: "always" as const,
carryBoxes: 'never' as const,
answerBoxes: 'always' as const,
placeValueColors: 'always' as const,
tenFrames: 'never' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
},
showBorrowNotation: true,
showBorrowingHints: false,
},
},
{
name: "subtraction-hints-detail",
filename: "hints-detail.svg",
description: "Detailed view of borrowing hints",
name: 'subtraction-hints-detail',
filename: 'hints-detail.svg',
description: 'Detailed view of borrowing hints',
config: {
operator: "subtraction" as const,
operator: 'subtraction' as const,
pAllStart: 0.0,
pAnyStart: 1.0,
digitRange: { min: 3, max: 3 },
mode: "manual" as const,
mode: 'manual' as const,
displayRules: {
carryBoxes: "never" as const,
answerBoxes: "always" as const,
placeValueColors: "always" as const,
tenFrames: "never" as const,
problemNumbers: "always" as const,
cellBorders: "always" as const,
carryBoxes: 'never' as const,
answerBoxes: 'always' as const,
placeValueColors: 'always' as const,
tenFrames: 'never' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
},
showBorrowNotation: true,
showBorrowingHints: true,
@@ -221,12 +216,12 @@ const examples = [
cols: 1,
},
},
] as const;
] as const
console.log("Generating subtraction example worksheets...\n");
console.log('Generating subtraction example worksheets...\n')
for (const example of examples) {
console.log(`Generating ${example.description}...`);
console.log(`Generating ${example.description}...`)
const config = {
problemsPerPage: 4,
@@ -234,29 +229,29 @@ for (const example of examples) {
cols: 2,
seed: 12345, // Fixed seed for consistent examples
...example.config, // Spread example config last so it can override defaults
};
}
try {
const result = generateWorksheetPreview(config);
const result = generateWorksheetPreview(config)
if (!result.success || !result.pages || result.pages.length === 0) {
console.error(`Failed to generate ${example.name}:`, result.error);
console.error(`Details:`, result.details);
continue;
console.error(`Failed to generate ${example.name}:`, result.error)
console.error(`Details:`, result.details)
continue
}
// Get the first page's SVG
const svg = result.pages[0];
const svg = result.pages[0]
// Save to file
const outputPath = path.join(outputDir, example.filename);
fs.writeFileSync(outputPath, svg, "utf-8");
const outputPath = path.join(outputDir, example.filename)
fs.writeFileSync(outputPath, svg, 'utf-8')
console.log(` ✓ Saved to ${outputPath}`);
console.log(` ✓ Saved to ${outputPath}`)
} catch (error) {
console.error(` ✗ Error generating ${example.name}:`, error);
console.error(` ✗ Error generating ${example.name}:`, error)
}
}
console.log("\nDone! Subtraction example worksheets generated.");
console.log(`\nFiles saved to: ${outputDir}`);
console.log('\nDone! Subtraction example worksheets generated.')
console.log(`\nFiles saved to: ${outputDir}`)

View File

@@ -13,37 +13,32 @@
// 3. Compile to SVG using typst
// 4. Save to public/blog/[your-post-name]/
import fs from "fs";
import path from "path";
import { execSync } from "child_process";
import fs from 'fs'
import path from 'path'
import { execSync } from 'child_process'
import {
generateTypstHelpers,
generateProblemStackFunction,
} from "../src/app/create/worksheets/addition/typstHelpers";
} from '../src/app/create/worksheets/addition/typstHelpers'
// Output directory
const outputDir = path.join(
process.cwd(),
"public",
"blog",
"ten-frame-examples",
);
const outputDir = path.join(process.cwd(), 'public', 'blog', 'ten-frame-examples')
// Ensure output directory exists
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
fs.mkdirSync(outputDir, { recursive: true })
}
interface ExampleOptions {
showCarryBoxes?: boolean;
showAnswerBoxes?: boolean;
showPlaceValueColors?: boolean;
showTenFrames?: boolean;
showProblemNumbers?: boolean;
transparentBackground?: boolean;
fontSize?: number;
addend1: number;
addend2: number;
showCarryBoxes?: boolean
showAnswerBoxes?: boolean
showPlaceValueColors?: boolean
showTenFrames?: boolean
showProblemNumbers?: boolean
transparentBackground?: boolean
fontSize?: number
addend1: number
addend2: number
}
/**
@@ -52,26 +47,26 @@ interface ExampleOptions {
* Extracted here so we can generate static examples for blog posts
*/
function generateExampleTypst(config: ExampleOptions): string {
const a = config.addend1;
const b = config.addend2;
const fontSize = config.fontSize || 16;
const cellSize = 0.45; // Slightly larger for blog examples vs UI previews (0.35)
const a = config.addend1
const b = config.addend2
const fontSize = config.fontSize || 16
const cellSize = 0.45 // Slightly larger for blog examples vs UI previews (0.35)
// Boolean flags matching worksheet generator
const showCarries = config.showCarryBoxes ?? false;
const showAnswers = config.showAnswerBoxes ?? false;
const showColors = config.showPlaceValueColors ?? false;
const showNumbers = config.showProblemNumbers ?? false;
const showTenFrames = config.showTenFrames ?? false;
const showTenFramesForAll = false; // Not used for blog examples
const transparentBg = config.transparentBackground ?? false;
const showCarries = config.showCarryBoxes ?? false
const showAnswers = config.showAnswerBoxes ?? false
const showColors = config.showPlaceValueColors ?? false
const showNumbers = config.showProblemNumbers ?? false
const showTenFrames = config.showTenFrames ?? false
const showTenFramesForAll = false // Not used for blog examples
const transparentBg = config.transparentBackground ?? false
return String.raw`
#set page(width: auto, height: auto, margin: 12pt, fill: ${transparentBg ? "none" : "white"})
#set page(width: auto, height: auto, margin: 12pt, fill: ${transparentBg ? 'none' : 'white'})
#set text(size: ${fontSize}pt, font: "New Computer Modern Math")
#let heavy-stroke = 0.8pt
#let show-ten-frames-for-all = ${showTenFramesForAll ? "true" : "false"}
#let show-ten-frames-for-all = ${showTenFramesForAll ? 'true' : 'false'}
${generateTypstHelpers(cellSize)}
@@ -87,7 +82,7 @@ ${generateProblemStackFunction(cellSize)}
#align(center + horizon)[
#problem-stack(
a, b, aT, aO, bT, bO,
${showNumbers ? "0" : "none"},
${showNumbers ? '0' : 'none'},
${showCarries},
${showAnswers},
${showColors},
@@ -95,16 +90,16 @@ ${generateProblemStackFunction(cellSize)}
${showNumbers}
)
]
`;
`
}
// Generate examples showing ten-frames in action
// Use problems that WILL have regrouping to show ten-frames
const examples = [
{
name: "with-ten-frames",
filename: "with-ten-frames.svg",
description: "With Ten-Frames: Visual scaffolding for regrouping",
name: 'with-ten-frames',
filename: 'with-ten-frames.svg',
description: 'With Ten-Frames: Visual scaffolding for regrouping',
options: {
addend1: 47,
addend2: 38, // 7+8=15 requires regrouping, will show ten-frames
@@ -117,9 +112,9 @@ const examples = [
},
},
{
name: "without-ten-frames",
filename: "without-ten-frames.svg",
description: "Without Ten-Frames: Abstract representation",
name: 'without-ten-frames',
filename: 'without-ten-frames.svg',
description: 'Without Ten-Frames: Abstract representation',
options: {
addend1: 47,
addend2: 38, // Same problem, no ten-frames
@@ -132,9 +127,9 @@ const examples = [
},
},
{
name: "beginner-with-ten-frames",
filename: "beginner-ten-frames.svg",
description: "Beginner: Learning regrouping with ten-frames",
name: 'beginner-with-ten-frames',
filename: 'beginner-ten-frames.svg',
description: 'Beginner: Learning regrouping with ten-frames',
options: {
addend1: 28,
addend2: 15, // 8+5=13 requires regrouping
@@ -147,9 +142,9 @@ const examples = [
},
},
{
name: "ten-frames-both-columns",
filename: "ten-frames-both-columns.svg",
description: "Ten-frames in both columns: Double regrouping",
name: 'ten-frames-both-columns',
filename: 'ten-frames-both-columns.svg',
description: 'Ten-frames in both columns: Double regrouping',
options: {
addend1: 57,
addend2: 68, // Both ones (7+8=15) and tens (5+6+1=12) regroup
@@ -161,22 +156,22 @@ const examples = [
transparentBackground: true,
},
},
] as const;
] as const
console.log("Generating ten-frame example images (single problems)...\n");
console.log('Generating ten-frame example images (single problems)...\n')
for (const example of examples) {
console.log(`Generating ${example.description}...`);
console.log(`Generating ${example.description}...`)
try {
const typstSource = generateExampleTypst(example.options);
const typstSource = generateExampleTypst(example.options)
// Compile to SVG
let svg = execSync("typst compile --format svg - -", {
let svg = execSync('typst compile --format svg - -', {
input: typstSource,
encoding: "utf8",
encoding: 'utf8',
maxBuffer: 2 * 1024 * 1024,
});
})
// Post-process: Make SVG visible on dark background
// - Digits on white cells should stay BLACK
@@ -184,23 +179,23 @@ for (const example of examples) {
// - Structural elements (borders, bars) should be WHITE
svg = svg
.replace(/stroke="#000000"/g, 'stroke="rgba(255, 255, 255, 0.8)"')
.replace(/stroke="#0000004d"/g, 'stroke="rgba(255, 255, 255, 0.4)"');
.replace(/stroke="#0000004d"/g, 'stroke="rgba(255, 255, 255, 0.4)"')
// Replace operator (+) fill specifically to white
svg = svg.replace(
/(<use xlink:href="#gCFEF70472F9D2AA9AC128F96529819DA"[^>]*fill=")#000000/g,
"$1rgba(255, 255, 255, 0.9)",
);
'$1rgba(255, 255, 255, 0.9)'
)
// Save to file
const outputPath = path.join(outputDir, example.filename);
fs.writeFileSync(outputPath, svg, "utf-8");
const outputPath = path.join(outputDir, example.filename)
fs.writeFileSync(outputPath, svg, 'utf-8')
console.log(` ✓ Saved to ${outputPath}`);
console.log(` ✓ Saved to ${outputPath}`)
} catch (error) {
console.error(` ✗ Error generating ${example.name}:`, error);
console.error(` ✗ Error generating ${example.name}:`, error)
}
}
console.log("\nDone! Ten-frame example images generated.");
console.log(`\nFiles saved to: ${outputDir}`);
console.log('\nDone! Ten-frame example images generated.')
console.log(`\nFiles saved to: ${outputDir}`)

View File

@@ -4,83 +4,83 @@
* Test script to parse the Rithmomachia board CSV and verify the layout.
*/
const fs = require("fs");
const path = require("path");
const fs = require('fs')
const path = require('path')
const csvPath = path.join(
process.env.HOME,
"Downloads",
"rithmomachia board setup - Sheet1 (1).csv",
);
'Downloads',
'rithmomachia board setup - Sheet1 (1).csv'
)
function parseCSV(csvContent) {
const lines = csvContent.trim().split("\n");
const pieces = [];
const lines = csvContent.trim().split('\n')
const pieces = []
// Process in triplets (color, shape, number)
for (let rankIndex = 0; rankIndex < 16; rankIndex++) {
const colorRowIndex = rankIndex * 3;
const shapeRowIndex = rankIndex * 3 + 1;
const numberRowIndex = rankIndex * 3 + 2;
const colorRowIndex = rankIndex * 3
const shapeRowIndex = rankIndex * 3 + 1
const numberRowIndex = rankIndex * 3 + 2
if (numberRowIndex >= lines.length) break;
if (numberRowIndex >= lines.length) break
const colorRow = lines[colorRowIndex].split(",");
const shapeRow = lines[shapeRowIndex].split(",");
const numberRow = lines[numberRowIndex].split(",");
const colorRow = lines[colorRowIndex].split(',')
const shapeRow = lines[shapeRowIndex].split(',')
const numberRow = lines[numberRowIndex].split(',')
// Process each column (8 total)
for (let colIndex = 0; colIndex < 8; colIndex++) {
const color = colorRow[colIndex]?.trim();
const shape = shapeRow[colIndex]?.trim();
const numberStr = numberRow[colIndex]?.trim();
const color = colorRow[colIndex]?.trim()
const shape = shapeRow[colIndex]?.trim()
const numberStr = numberRow[colIndex]?.trim()
// Skip empty cells (but allow empty number for Pyramids)
if (!color || !shape) continue;
if (!color || !shape) continue
// Map CSV position to game square
// CSV column → game row (1-8)
// CSV rank → game column (A-P)
const gameRow = colIndex + 1; // CSV col 0 → row 1, col 7 → row 8
const gameCol = String.fromCharCode(65 + rankIndex); // rank 0 → A, rank 15 → P
const square = `${gameCol}${gameRow}`;
const gameRow = colIndex + 1 // CSV col 0 → row 1, col 7 → row 8
const gameCol = String.fromCharCode(65 + rankIndex) // rank 0 → A, rank 15 → P
const square = `${gameCol}${gameRow}`
// Parse color
const pieceColor = color.toLowerCase() === "black" ? "B" : "W";
const pieceColor = color.toLowerCase() === 'black' ? 'B' : 'W'
// Parse type
let pieceType;
const shapeLower = shape.toLowerCase();
if (shapeLower === "circle") pieceType = "C";
else if (shapeLower === "triangle" || shapeLower === "traingle")
pieceType = "T"; // Handle typo
else if (shapeLower === "square") pieceType = "S";
else if (shapeLower === "pyramid") pieceType = "P";
let pieceType
const shapeLower = shape.toLowerCase()
if (shapeLower === 'circle') pieceType = 'C'
else if (shapeLower === 'triangle' || shapeLower === 'traingle')
pieceType = 'T' // Handle typo
else if (shapeLower === 'square') pieceType = 'S'
else if (shapeLower === 'pyramid') pieceType = 'P'
else {
console.warn(`Unknown shape "${shape}" at ${square}`);
continue;
console.warn(`Unknown shape "${shape}" at ${square}`)
continue
}
// Parse value/pyramid faces
if (pieceType === "P") {
if (pieceType === 'P') {
// Pyramid - number cell should be empty, use default faces
pieces.push({
color: pieceColor,
type: pieceType,
pyramidFaces: pieceColor === "B" ? [36, 25, 16, 4] : [64, 49, 36, 25],
pyramidFaces: pieceColor === 'B' ? [36, 25, 16, 4] : [64, 49, 36, 25],
square,
});
})
} else {
// Regular piece needs a number
if (!numberStr) {
console.warn(`Missing number for non-Pyramid ${shape} at ${square}`);
continue;
console.warn(`Missing number for non-Pyramid ${shape} at ${square}`)
continue
}
const value = parseInt(numberStr, 10);
const value = parseInt(numberStr, 10)
if (isNaN(value)) {
console.warn(`Invalid number "${numberStr}" at ${square}`);
continue;
console.warn(`Invalid number "${numberStr}" at ${square}`)
continue
}
pieces.push({
@@ -88,120 +88,119 @@ function parseCSV(csvContent) {
type: pieceType,
value,
square,
});
})
}
}
}
return pieces;
return pieces
}
function generateBoardDisplay(pieces) {
const lines = [];
const lines = []
lines.push("\n=== Board Layout (Game Orientation) ===");
lines.push("BLACK (top)\n");
lines.push('\n=== Board Layout (Game Orientation) ===')
lines.push('BLACK (top)\n')
lines.push(
" A B C D E F G H I J K L M N O P",
);
' A B C D E F G H I J K L M N O P'
)
for (let row = 8; row >= 1; row--) {
let line = `${row} `;
let line = `${row} `
for (let colCode = 65; colCode <= 80; colCode++) {
const col = String.fromCharCode(colCode);
const square = `${col}${row}`;
const piece = pieces.find((p) => p.square === square);
const col = String.fromCharCode(colCode)
const square = `${col}${row}`
const piece = pieces.find((p) => p.square === square)
if (piece) {
const val =
piece.type === "P" ? " P" : piece.value.toString().padStart(3, " ");
line += ` ${piece.color}${piece.type}${val} `;
const val = piece.type === 'P' ? ' P' : piece.value.toString().padStart(3, ' ')
line += ` ${piece.color}${piece.type}${val} `
} else {
line += " ---- ";
line += ' ---- '
}
}
lines.push(line);
lines.push(line)
}
lines.push("\nWHITE (bottom)\n");
lines.push('\nWHITE (bottom)\n')
return lines.join("\n");
return lines.join('\n')
}
function generateColumnSummaries(pieces) {
const lines = [];
const lines = []
lines.push("\n=== Column-by-Column Summary ===\n");
lines.push('\n=== Column-by-Column Summary ===\n')
for (let colCode = 65; colCode <= 80; colCode++) {
const col = String.fromCharCode(colCode);
const col = String.fromCharCode(colCode)
const columnPieces = pieces
.filter((p) => p.square[0] === col)
.sort((a, b) => {
const rowA = parseInt(a.square.substring(1));
const rowB = parseInt(b.square.substring(1));
return rowA - rowB;
});
const rowA = parseInt(a.square.substring(1))
const rowB = parseInt(b.square.substring(1))
return rowA - rowB
})
if (columnPieces.length === 0) continue;
if (columnPieces.length === 0) continue
const color = columnPieces[0].color === "B" ? "BLACK" : "WHITE";
lines.push(`Column ${col} (${color}):`);
const color = columnPieces[0].color === 'B' ? 'BLACK' : 'WHITE'
lines.push(`Column ${col} (${color}):`)
for (const piece of columnPieces) {
const val = piece.type === "P" ? "P[36,25,16,4]" : piece.value;
lines.push(` ${piece.square}: ${piece.type}(${val})`);
const val = piece.type === 'P' ? 'P[36,25,16,4]' : piece.value
lines.push(` ${piece.square}: ${piece.type}(${val})`)
}
lines.push("");
lines.push('')
}
return lines.join("\n");
return lines.join('\n')
}
function countPieces(pieces) {
const blackPieces = pieces.filter((p) => p.color === "B");
const whitePieces = pieces.filter((p) => p.color === "W");
const blackPieces = pieces.filter((p) => p.color === 'B')
const whitePieces = pieces.filter((p) => p.color === 'W')
const countByType = (pieces) => {
const counts = { C: 0, T: 0, S: 0, P: 0 };
for (const p of pieces) counts[p.type]++;
return counts;
};
const counts = { C: 0, T: 0, S: 0, P: 0 }
for (const p of pieces) counts[p.type]++
return counts
}
const blackCounts = countByType(blackPieces);
const whiteCounts = countByType(whitePieces);
const blackCounts = countByType(blackPieces)
const whiteCounts = countByType(whitePieces)
console.log("\n=== Piece Counts ===");
console.log('\n=== Piece Counts ===')
console.log(
`Black: ${blackPieces.length} total (C:${blackCounts.C}, T:${blackCounts.T}, S:${blackCounts.S}, P:${blackCounts.P})`,
);
`Black: ${blackPieces.length} total (C:${blackCounts.C}, T:${blackCounts.T}, S:${blackCounts.S}, P:${blackCounts.P})`
)
console.log(
`White: ${whitePieces.length} total (C:${whiteCounts.C}, T:${whiteCounts.T}, S:${whiteCounts.S}, P:${whiteCounts.P})`,
);
`White: ${whitePieces.length} total (C:${whiteCounts.C}, T:${whiteCounts.T}, S:${whiteCounts.S}, P:${whiteCounts.P})`
)
}
// Main
try {
const csvContent = fs.readFileSync(csvPath, "utf-8");
const pieces = parseCSV(csvContent);
const csvContent = fs.readFileSync(csvPath, 'utf-8')
const pieces = parseCSV(csvContent)
console.log(`\nParsed ${pieces.length} pieces from CSV`);
console.log(generateBoardDisplay(pieces));
console.log(generateColumnSummaries(pieces));
countPieces(pieces);
console.log(`\nParsed ${pieces.length} pieces from CSV`)
console.log(generateBoardDisplay(pieces))
console.log(generateColumnSummaries(pieces))
countPieces(pieces)
// Save parsed data
const outputPath = path.join(
__dirname,
"..",
"src",
"arcade-games",
"rithmomachia",
"utils",
"parsedBoard.json",
);
fs.writeFileSync(outputPath, JSON.stringify(pieces, null, 2));
console.log(`\n✅ Saved parsed board to: ${outputPath}`);
'..',
'src',
'arcade-games',
'rithmomachia',
'utils',
'parsedBoard.json'
)
fs.writeFileSync(outputPath, JSON.stringify(pieces, null, 2))
console.log(`\n✅ Saved parsed board to: ${outputPath}`)
} catch (error) {
console.error("Error:", error.message);
process.exit(1);
console.error('Error:', error.message)
process.exit(1)
}

View File

@@ -6,104 +6,102 @@ import {
findScaffoldingIndex,
REGROUPING_PROGRESSION,
SCAFFOLDING_PROGRESSION,
} from "../src/app/create/worksheets/addition/difficultyProfiles";
} from '../src/app/create/worksheets/addition/difficultyProfiles'
// Start from beginner
let state = {
pAnyStart: DIFFICULTY_PROFILES.beginner.regrouping.pAnyStart,
pAllStart: DIFFICULTY_PROFILES.beginner.regrouping.pAllStart,
displayRules: DIFFICULTY_PROFILES.beginner.displayRules,
};
}
console.log("=== MAKE HARDER PATH ===\n");
console.log("Format: (regroupingIdx, scaffoldingIdx) - description\n");
console.log('=== MAKE HARDER PATH ===\n')
console.log('Format: (regroupingIdx, scaffoldingIdx) - description\n')
const harderPath: Array<{ r: number; s: number; desc: string }> = [];
const harderPath: Array<{ r: number; s: number; desc: string }> = []
// Record starting point
let rIdx = findRegroupingIndex(state.pAnyStart, state.pAllStart);
let sIdx = findScaffoldingIndex(state.displayRules);
harderPath.push({ r: rIdx, s: sIdx, desc: "START (beginner)" });
console.log(`(${rIdx}, ${sIdx}) - START (beginner)`);
let rIdx = findRegroupingIndex(state.pAnyStart, state.pAllStart)
let sIdx = findScaffoldingIndex(state.displayRules)
harderPath.push({ r: rIdx, s: sIdx, desc: 'START (beginner)' })
console.log(`(${rIdx}, ${sIdx}) - START (beginner)`)
// Click "Make Harder" 30 times or until max
for (let i = 0; i < 30; i++) {
const result = makeHarder(state);
const result = makeHarder(state)
const newR = findRegroupingIndex(result.pAnyStart, result.pAllStart);
const newS = findScaffoldingIndex(result.displayRules);
const newR = findRegroupingIndex(result.pAnyStart, result.pAllStart)
const newS = findScaffoldingIndex(result.displayRules)
if (newR === rIdx && newS === sIdx) {
console.log(`\n(${newR}, ${newS}) - ${result.changeDescription} (STOPPED)`);
break;
console.log(`\n(${newR}, ${newS}) - ${result.changeDescription} (STOPPED)`)
break
}
rIdx = newR;
sIdx = newS;
state = result;
rIdx = newR
sIdx = newS
state = result
harderPath.push({ r: rIdx, s: sIdx, desc: result.changeDescription });
console.log(`(${rIdx}, ${sIdx}) - ${result.changeDescription}`);
harderPath.push({ r: rIdx, s: sIdx, desc: result.changeDescription })
console.log(`(${rIdx}, ${sIdx}) - ${result.changeDescription}`)
}
console.log("\n\n=== PATH VISUALIZATION ===\n");
console.log("Regrouping Index →");
console.log("Scaffolding ↓\n");
console.log('\n\n=== PATH VISUALIZATION ===\n')
console.log('Regrouping Index →')
console.log('Scaffolding ↓\n')
// Create 2D grid visualization
const grid: string[][] = [];
const grid: string[][] = []
for (let s = 0; s <= 12; s++) {
grid[s] = [];
grid[s] = []
for (let r = 0; r <= 18; r++) {
grid[s][r] = " ·";
grid[s][r] = ' ·'
}
}
// Mark path
harderPath.forEach((point, idx) => {
if (idx === 0) {
grid[point.s][point.r] = " S"; // Start
grid[point.s][point.r] = ' S' // Start
} else if (idx === harderPath.length - 1) {
grid[point.s][point.r] = " E"; // End
grid[point.s][point.r] = ' E' // End
} else {
grid[point.s][point.r] = `${idx.toString().padStart(3)}`;
grid[point.s][point.r] = `${idx.toString().padStart(3)}`
}
});
})
// Mark presets
const presets = [
{ label: "BEG", profile: DIFFICULTY_PROFILES.beginner },
{ label: "EAR", profile: DIFFICULTY_PROFILES.earlyLearner },
{ label: "INT", profile: DIFFICULTY_PROFILES.intermediate },
{ label: "ADV", profile: DIFFICULTY_PROFILES.advanced },
{ label: "EXP", profile: DIFFICULTY_PROFILES.expert },
];
{ label: 'BEG', profile: DIFFICULTY_PROFILES.beginner },
{ label: 'EAR', profile: DIFFICULTY_PROFILES.earlyLearner },
{ label: 'INT', profile: DIFFICULTY_PROFILES.intermediate },
{ label: 'ADV', profile: DIFFICULTY_PROFILES.advanced },
{ label: 'EXP', profile: DIFFICULTY_PROFILES.expert },
]
presets.forEach((preset) => {
const r = findRegroupingIndex(
preset.profile.regrouping.pAnyStart,
preset.profile.regrouping.pAllStart,
);
const s = findScaffoldingIndex(preset.profile.displayRules);
preset.profile.regrouping.pAllStart
)
const s = findScaffoldingIndex(preset.profile.displayRules)
// Only mark if not already part of path
const onPath = harderPath.some((p) => p.r === r && p.s === s);
const onPath = harderPath.some((p) => p.r === r && p.s === s)
if (!onPath) {
grid[s][r] = preset.label;
grid[s][r] = preset.label
}
});
})
// Print grid (inverted so scaffolding increases upward)
console.log(
" 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18",
);
console.log(' 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18')
for (let s = 12; s >= 0; s--) {
console.log(`${s.toString().padStart(2)} ${grid[s].join("")}`);
console.log(`${s.toString().padStart(2)} ${grid[s].join('')}`)
}
console.log("\nLegend:");
console.log(" S = Start (beginner)");
console.log(" E = End (maximum)");
console.log(" 1-29 = Step number");
console.log(" BEG/EAR/INT/ADV/EXP = Preset profiles");
console.log(" · = Not visited");
console.log('\nLegend:')
console.log(' S = Start (beginner)')
console.log(' E = End (maximum)')
console.log(' 1-29 = Step number')
console.log(' BEG/EAR/INT/ADV/EXP = Preset profiles')
console.log(' · = Not visited')

View File

@@ -6,98 +6,93 @@
* produces identical Typst output to ensure no regressions.
*/
import { generateSubtractionProblemStackFunction } from "../src/app/create/worksheets/addition/typstHelpers";
import { generateTypstHelpers } from "../src/app/create/worksheets/addition/typstHelpers";
import { generatePlaceValueColors } from "../src/app/create/worksheets/addition/typstHelpers";
import { generateSubtractionProblemStackFunction } from '../src/app/create/worksheets/addition/typstHelpers'
import { generateTypstHelpers } from '../src/app/create/worksheets/addition/typstHelpers'
import { generatePlaceValueColors } from '../src/app/create/worksheets/addition/typstHelpers'
console.log("🔍 Validating typstHelpers refactoring...\n");
console.log('🔍 Validating typstHelpers refactoring...\n')
// Test 1: Check that functions are exported and callable
console.log("✓ Test 1: Functions are exported");
console.log('✓ Test 1: Functions are exported')
console.log(
` - generateSubtractionProblemStackFunction: ${typeof generateSubtractionProblemStackFunction}`,
);
console.log(` - generateTypstHelpers: ${typeof generateTypstHelpers}`);
console.log(` - generatePlaceValueColors: ${typeof generatePlaceValueColors}`);
` - generateSubtractionProblemStackFunction: ${typeof generateSubtractionProblemStackFunction}`
)
console.log(` - generateTypstHelpers: ${typeof generateTypstHelpers}`)
console.log(` - generatePlaceValueColors: ${typeof generatePlaceValueColors}`)
if (typeof generateSubtractionProblemStackFunction !== "function") {
console.error(
"❌ generateSubtractionProblemStackFunction is not a function!",
);
process.exit(1);
if (typeof generateSubtractionProblemStackFunction !== 'function') {
console.error('❌ generateSubtractionProblemStackFunction is not a function!')
process.exit(1)
}
// Test 2: Generate sample Typst code
console.log("\n✓ Test 2: Generate sample Typst code");
const cellSize = 0.55;
const maxDigits = 3;
console.log('\n✓ Test 2: Generate sample Typst code')
const cellSize = 0.55
const maxDigits = 3
const helpers = generateTypstHelpers(cellSize);
console.log(` - Helper functions: ${helpers.length} characters`);
const helpers = generateTypstHelpers(cellSize)
console.log(` - Helper functions: ${helpers.length} characters`)
const colors = generatePlaceValueColors();
console.log(` - Color definitions: ${colors.length} characters`);
const colors = generatePlaceValueColors()
console.log(` - Color definitions: ${colors.length} characters`)
const problemStack = generateSubtractionProblemStackFunction(
cellSize,
maxDigits,
);
console.log(` - Problem stack function: ${problemStack.length} characters`);
const problemStack = generateSubtractionProblemStackFunction(cellSize, maxDigits)
console.log(` - Problem stack function: ${problemStack.length} characters`)
// Test 3: Verify key features are present
console.log("\n✓ Test 3: Verify key features in generated Typst");
console.log('\n✓ Test 3: Verify key features in generated Typst')
const checks = [
{ name: "Borrow boxes row", pattern: /Borrow boxes row/ },
{ name: "Minuend row", pattern: /Minuend row/ },
{ name: "Subtrahend row", pattern: /Subtrahend row/ },
{ name: "Answer boxes", pattern: /Answer boxes/ },
{ name: "Ten-frames", pattern: /Ten-frames row/ },
{ name: "Borrowing hints", pattern: /show-borrowing-hints/ },
{ name: "Arrow rendering", pattern: /path\(/ },
{ name: "Place value colors", pattern: /place-colors/ },
{ name: "Scratch work boxes", pattern: /dotted.*paint: gray/ },
];
{ name: 'Borrow boxes row', pattern: /Borrow boxes row/ },
{ name: 'Minuend row', pattern: /Minuend row/ },
{ name: 'Subtrahend row', pattern: /Subtrahend row/ },
{ name: 'Answer boxes', pattern: /Answer boxes/ },
{ name: 'Ten-frames', pattern: /Ten-frames row/ },
{ name: 'Borrowing hints', pattern: /show-borrowing-hints/ },
{ name: 'Arrow rendering', pattern: /path\(/ },
{ name: 'Place value colors', pattern: /place-colors/ },
{ name: 'Scratch work boxes', pattern: /dotted.*paint: gray/ },
]
let allPassed = true;
let allPassed = true
for (const check of checks) {
const found = check.pattern.test(problemStack);
const found = check.pattern.test(problemStack)
if (found) {
console.log(`${check.name}`);
console.log(`${check.name}`)
} else {
console.log(`${check.name} - NOT FOUND`);
allPassed = false;
console.log(`${check.name} - NOT FOUND`)
allPassed = false
}
}
// Test 4: Verify structure
console.log("\n✓ Test 4: Verify Typst structure");
console.log('\n✓ Test 4: Verify Typst structure')
const structureChecks = [
{ name: "Function definition", pattern: /#let subtraction-problem-stack\(/ },
{ name: "Grid structure", pattern: /grid\(/ },
{ name: "Stack structure", pattern: /stack\(/ },
{ name: "Problem number display", pattern: /problem-number-display/ },
];
{ name: 'Function definition', pattern: /#let subtraction-problem-stack\(/ },
{ name: 'Grid structure', pattern: /grid\(/ },
{ name: 'Stack structure', pattern: /stack\(/ },
{ name: 'Problem number display', pattern: /problem-number-display/ },
]
for (const check of structureChecks) {
const found = check.pattern.test(problemStack);
const found = check.pattern.test(problemStack)
if (found) {
console.log(`${check.name}`);
console.log(`${check.name}`)
} else {
console.log(`${check.name} - NOT FOUND`);
allPassed = false;
console.log(`${check.name} - NOT FOUND`)
allPassed = false
}
}
// Summary
console.log("\n" + "=".repeat(60));
console.log('\n' + '='.repeat(60))
if (allPassed) {
console.log("✅ All validation checks passed!");
console.log("\nThe refactored code generates valid Typst output with all");
console.log("expected features present.");
process.exit(0);
console.log('✅ All validation checks passed!')
console.log('\nThe refactored code generates valid Typst output with all')
console.log('expected features present.')
process.exit(0)
} else {
console.log("❌ Some validation checks failed!");
console.log("\nPlease review the output above for details.");
process.exit(1);
console.log('❌ Some validation checks failed!')
console.log('\nPlease review the output above for details.')
process.exit(1)
}