diff options
Diffstat (limited to 'html')
-rw-r--r-- | html/bp-sand/index.html | 164 | ||||
-rw-r--r-- | html/broughlike/about.html | 28 | ||||
-rw-r--r-- | html/broughlike/index.html | 546 | ||||
-rw-r--r-- | html/file-system/index.html | 297 | ||||
-rw-r--r-- | html/playground/APL386.ttf | bin | 0 -> 203668 bytes | |||
-rw-r--r-- | html/playground/index.html | 243 | ||||
-rw-r--r-- | html/playground/little-regex.html | 728 | ||||
-rw-r--r-- | html/playground/regex.html | 477 | ||||
-rw-r--r-- | html/playground/scheme.html | 533 | ||||
-rw-r--r-- | html/schemer/index.html | 305 | ||||
-rw-r--r-- | html/schemer/tls.pdf | bin | 0 -> 2359953 bytes | |||
-rw-r--r-- | html/squine.html | 1108 | ||||
-rw-r--r-- | html/tuner/index.html | 193 |
13 files changed, 4622 insertions, 0 deletions
diff --git a/html/bp-sand/index.html b/html/bp-sand/index.html new file mode 100644 index 0000000..e34e33c --- /dev/null +++ b/html/bp-sand/index.html @@ -0,0 +1,164 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>BP Sand</title> + <style> + body, html { + margin: 0; + padding: 0; + overflow: hidden; + display: flex; + flex-direction: column; + height: 100%; + } + canvas { + display: block; + width: 100%; + background-color: beige; + } + .controls { + display: flex; + justify-content: space-around; + padding: 10px; + background-color: white; + box-shadow: 0 -2px 5px rgba(0,0,0,0.1); + } + input, button { + font-size: 1.5rem; + } + </style> +</head> +<body> + <canvas id="sandCanvas"></canvas> + <div class="controls"> + <input type="number" id="bpmInput" value="60" min="10" max="300"> + <button id="toggleButton">Start</button> + </div> + + <script> + const canvas = document.getElementById('sandCanvas'); + const ctx = canvas.getContext('2d'); + const bpmInput = document.getElementById('bpmInput'); + const toggleButton = document.getElementById('toggleButton'); + const controls = document.querySelector('.controls'); + + let bpm = 60; + let isRunning = false; + let sandParticles = []; + let intervalId; + + // Set canvas size to avoid overlapping with controls + function resizeCanvas() { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight - controls.offsetHeight; + } + + window.addEventListener('resize', resizeCanvas); + resizeCanvas(); + + const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + + function playTone() { + const oscillator = audioCtx.createOscillator(); + oscillator.type = 'sine'; + oscillator.frequency.setValueAtTime(440, audioCtx.currentTime); // 440 Hz tone + oscillator.connect(audioCtx.destination); + oscillator.start(); + oscillator.stop(audioCtx.currentTime + 0.1); // Short beep + } + + function createSandParticle() { + return { + x: Math.random() * canvas.width, + y: 0, + size: 5 + Math.random() * 5, // Vary size a bit + velocityY: 0, + atRest: false // Track if the particle is at rest + }; + } + + // Update sand particle positions + function updateSand() { + for (let particle of sandParticles) { + if (!particle.atRest) { + particle.velocityY += 0.1; // Accelerate! Gravity! + particle.y += particle.velocityY; + + // Check if sand particle has hit the bottom + if (particle.y + particle.size >= canvas.height) { + particle.y = canvas.height - particle.size; + particle.atRest = true; // Mark the particle as at rest + } + + // Check if sand particle has landed on another particle + for (let otherParticle of sandParticles) { + if (otherParticle !== particle && otherParticle.atRest) { + let distY = otherParticle.y - (particle.y + particle.size); + let distX = Math.abs(otherParticle.x - particle.x); + if (distY <= 0 && distX < particle.size) { + particle.y = otherParticle.y - particle.size; + particle.atRest = true; + break; + } + } + } + } + } + } + + // Draw sand particles on the canvas + function drawSand() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = 'teal'; + + for (let particle of sandParticles) { + ctx.beginPath(); + ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2); + ctx.fill(); + } + } + + // Main loop + function gameLoop() { + updateSand(); + drawSand(); + } + + // Control the BPM and start/stop the loop + function toggleBPM() { + isRunning = !isRunning; + if (isRunning) { + let interval = (60 / bpm) * 1000; // Convert BPM to interval in milliseconds + intervalId = setInterval(() => { + sandParticles.push(createSandParticle()); // Generate new sand + playTone(); // Play the tone + }, interval); + toggleButton.textContent = 'Stop'; + } else { + clearInterval(intervalId); + toggleButton.textContent = 'Start'; + } + } + + // Event Listeners + toggleButton.addEventListener('click', toggleBPM); + + bpmInput.addEventListener('input', (e) => { + bpm = parseInt(e.target.value); + if (isRunning) { + clearInterval(intervalId); + toggleBPM(); + } + }); + + function animate() { + gameLoop(); + requestAnimationFrame(animate); + } + + animate(); + </script> +</body> +</html> diff --git a/html/broughlike/about.html b/html/broughlike/about.html new file mode 100644 index 0000000..0811f77 --- /dev/null +++ b/html/broughlike/about.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>About Eli's Broughlike</title> + <meta description="A Broughlike, or something like Flatland."> + <style> + body { + background-color: #f0f0f0; + padding: 1em 2em; + } + </style> +</head> +<body> + <h1>How to play</h1> + <ul> + <li>You are the hexagon.</li> + <li>Your goal is to reach the triangle. When you reach the triangle you'll be brought to the next level.</li> + <li>Enemies are circles. Run into them to battle. Be warned, you'll also take damage.</li> + <li>Diamonds add 3 to the amount of damage you can do to an enemy for the level.</li> + <li>Pentagons heal you a little bit.</li> + <li>You can tell your or an enemy's health by the opacity of the shape. Darker means more health.</li> + <li>Arrow keys, WASD, or VIM movement keys to move on a thing with a keyboard, swipe up, down, left or right to move on something with a touchscreen.</li> + </ul> + <p><a href="index.html">Play the game!</a></p> +</body> +</html> \ No newline at end of file diff --git a/html/broughlike/index.html b/html/broughlike/index.html new file mode 100644 index 0000000..e643527 --- /dev/null +++ b/html/broughlike/index.html @@ -0,0 +1,546 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Eli's Broughlike</title> + <meta description="A Broughlike, or something like Flatland."> + <style> + body { + background-color: #f0f0f0; + } + canvas { + width: 90vw; + height: 90vw; + max-width: 600px; + max-height: 600px; + border: 2px solid black; + display: block; + margin: 0 auto; + background-color: #f0f0f0; + } + </style> +</head> +<body> + <p><a href="about.html">About</a></p> + <canvas id="gameCanvas"></canvas> + <script> + + const COLORS = { + grid: '#2d2d2d', + walls: '#2d2d2d', + exit: 'teal', + diamond: 'gold', + pentagon: 'blueviolet', + player: 'rgba(0, 237, 209, ', + enemy: 'rgba(255, 155, 28, ', + combatDotPlayer: '#00edd1', + combatDotEnemy: '#ff731c' + }; + + const GRID_SIZE = 6; + const PLAYER_HEALTH = 10; + const PLAYER_MAX_HEALTH = 12; + const PLAYER_BASE_DAMAGE = 1; + const ENEMY_CHASE_DISTANCE = 4; + const MAX_ENEMIES_ON_LEVEL = 4; + const MAX_ENEMY_HEALTH = 7; + const MIN_ENEMY_HEALTH = 2; + const WALLS_MIN = 5; + const WALLS_MAX = 20; + const ITEMS_MIN = 0; + const ITEMS_MAX = 3; + const DOTS_PER_HIT = 5; + + const canvas = document.getElementById('gameCanvas'); + const ctx = canvas.getContext('2d'); + let tileSize = canvas.width / GRID_SIZE; + + const player = { + x: 0, + y: 0, + health: PLAYER_HEALTH, + score: 0, + damage: PLAYER_BASE_DAMAGE, + totalDamageDone: 0, + totalDamageTaken: 0, + cellsTraveled: 0, + killCount: 0, + itemsCollected: 0 + }; + + const exit = { x: Math.floor(Math.random() * GRID_SIZE), y: Math.floor(Math.random() * GRID_SIZE) }; + let walls = []; + let enemies = []; + let items = []; + let combatDots = {}; + + function isValidMove(newX, newY) { + return ( + newX >= 0 && newX < GRID_SIZE && + newY >= 0 && newY < GRID_SIZE && + !walls.some(wall => wall.x === newX && wall.y === newY) + ); + } + + function generateExit() { + let distance = 0; + do { + exit.x = Math.floor(Math.random() * GRID_SIZE); + exit.y = Math.floor(Math.random() * GRID_SIZE); + distance = Math.abs(exit.x - player.x) + Math.abs(exit.y - player.y); + } while (distance < 4); + } + + function generateEnemies() { + enemies = []; + const numEnemies = Math.floor(Math.random() * (MAX_ENEMIES_ON_LEVEL + 1)); // Between 0 and the constant value. + for (let i = 0; i < numEnemies; i++) { + let enemyX, enemyY; + do { + enemyX = Math.floor(Math.random() * GRID_SIZE); + enemyY = Math.floor(Math.random() * GRID_SIZE); + } while ( + (enemyX === player.x && enemyY === player.y) || + (enemyX === exit.x && enemyY === exit.y) || + walls.some(wall => wall.x === enemyX && wall.y === enemyY) + ); + enemies.push({ + x: enemyX, + y: enemyY, + health: Math.floor(Math.random() * (MAX_ENEMY_HEALTH - MIN_ENEMY_HEALTH + 1)) + MIN_ENEMY_HEALTH + }); + } + } + + function generateWalls() { + walls = []; + let numWalls = Math.floor(Math.random() * (WALLS_MAX - WALLS_MIN + 1)) + WALLS_MIN; + while (walls.length < numWalls) { + const wallX = Math.floor(Math.random() * GRID_SIZE); + const wallY = Math.floor(Math.random() * GRID_SIZE); + + if ( + (wallX === player.x && wallY === player.y) || + (wallX === exit.x && wallY === exit.y) || + enemies.some(enemy => enemy.x === wallX && enemy.y === wallY) || + (wallX === 0 && wallY === 0) || // Don't block the player spawn + items.some(item => item.x === wallX && item.y === wallY) + ) continue; + + if (!walls.some(wall => wall.x === wallX && wall.y === wallY)) { + walls.push({ x: wallX, y: wallY }); + } + } + + if (!isPassable()) { + generateWalls(); + } + } + + function generateItems() { + items = []; + const numItems = Math.floor(Math.random() * (ITEMS_MAX - ITEMS_MIN + 1)) + ITEMS_MIN; + for (let i = 0; i < numItems; i++) { + let itemX, itemY; + do { + itemX = Math.floor(Math.random() * GRID_SIZE); + itemY = Math.floor(Math.random() * GRID_SIZE); + } while ( + (itemX === player.x && itemY === player.y) || + (itemX === exit.x && itemY === exit.y) || + walls.some(wall => wall.x === itemX && wall.y === itemY) || + enemies.some(enemy => enemy.x === itemX && enemy.y === itemY) + ); + const itemType = Math.random() < 0.5 ? 'diamond' : 'pentagon'; // 50% chance for each type + items.push({ x: itemX, y: itemY, type: itemType }); + } + } + + function isPassable() { + const visited = Array(GRID_SIZE).fill().map(() => Array(GRID_SIZE).fill(false)); + + function dfs(x, y) { + if (x < 0 || x >= GRID_SIZE || y < 0 || y >= GRID_SIZE) return false; + if (visited[x][y]) return false; + if (walls.some(wall => wall.x === x && wall.y === y)) return false; + visited[x][y] = true; + if (x === exit.x && y === exit.y) return true; + + return dfs(x + 1, y) || dfs(x - 1, y) || dfs(x, y + 1) || dfs(x, y - 1); + } + + return dfs(player.x, player.y); + } + + function drawGrid() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.strokeStyle = COLORS.grid; + for (let row = 0; row < GRID_SIZE; row++) { + for (let col = 0; col < GRID_SIZE; col++) { + ctx.strokeRect(col * tileSize, row * tileSize, tileSize, tileSize); + } + } + } + + function drawExit() { + const x = exit.x * tileSize + tileSize / 2; + const y = exit.y * tileSize + tileSize / 2; + ctx.beginPath(); + ctx.moveTo(x, y - tileSize / 3); + ctx.lineTo(x + tileSize / 3, y + tileSize / 3); + ctx.lineTo(x - tileSize / 3, y + tileSize / 3); + ctx.closePath(); + ctx.fillStyle = COLORS.exit; + ctx.fill(); + } + + function drawWalls() { + ctx.fillStyle = COLORS.walls; + walls.forEach(wall => { + ctx.fillRect(wall.x * tileSize, wall.y * tileSize, tileSize, tileSize); + }); + } + + function drawItems() { + items.forEach(item => { + const x = item.x * tileSize + tileSize / 2; + const y = item.y * tileSize + tileSize / 2; + ctx.fillStyle = item.type === 'diamond' ? COLORS.diamond : COLORS.pentagon; + ctx.beginPath(); + if (item.type === 'diamond') { + ctx.moveTo(x, y - tileSize / 4); + ctx.lineTo(x + tileSize / 4, y); + ctx.lineTo(x, y + tileSize / 4); + ctx.lineTo(x - tileSize / 4, y); + } else { + const sides = 5; + const radius = tileSize / 4; + for (let i = 0; i < sides; i++) { + const angle = (i * 2 * Math.PI) / sides - Math.PI / 2; + const pointX = x + radius * Math.cos(angle); + const pointY = y + radius * Math.sin(angle); + if (i === 0) ctx.moveTo(pointX, pointY); + else ctx.lineTo(pointX, pointY); + } + } + ctx.closePath(); + ctx.fill(); + }); + } + + function drawCharacterBorder(x, y, radius, damageTaken) { + const dashLength = 5; + const gapLength = Math.max(1, damageTaken * 2); // More damage, larger gaps + + ctx.lineWidth = 2; + ctx.strokeStyle = '#2d2d2d'; + ctx.setLineDash([dashLength, gapLength]); + ctx.beginPath(); + ctx.arc(x, y, radius, 0, 2 * Math.PI); + ctx.stroke(); + ctx.setLineDash([]); // Reset to a solid line + } + + function drawEnemies() { + enemies.forEach(enemy => { + const x = enemy.x * tileSize + tileSize / 2; + const y = enemy.y * tileSize + tileSize / 2; + const opacity = enemy.health / MAX_ENEMY_HEALTH; // Opacity based on health + const radius = tileSize / 3; + const damageTaken = MAX_ENEMY_HEALTH - enemy.health; + + ctx.beginPath(); + ctx.arc(x, y, radius, 0, 2 * Math.PI); + ctx.fillStyle = `${COLORS.enemy}${opacity})`; + ctx.fill(); + + drawCharacterBorder(x, y, radius, damageTaken); + }); + } + + function drawPlayer() { + const x = player.x * tileSize + tileSize / 2; + const y = player.y * tileSize + tileSize / 2; + const radius = tileSize / 3; + const playerOpacity = player.health / PLAYER_HEALTH; // Opacity based on health + ctx.beginPath(); + for (let i = 0; i < 6; i++) { + const angle = (Math.PI / 3) * i; + const hexX = x + radius * Math.cos(angle); + const hexY = y + radius * Math.sin(angle); + if (i === 0) { + ctx.moveTo(hexX, hexY); + } else { + ctx.lineTo(hexX, hexY); + } + } + ctx.closePath(); + ctx.fillStyle = `${COLORS.player}${playerOpacity})`; + ctx.fill(); + ctx.lineWidth = 2; + ctx.strokeStyle = '#2d2d2d'; + ctx.stroke(); + } + + function drawCombatDots() { + for (const key in combatDots) { + const [cellX, cellY] = key.split(',').map(Number); + const dots = combatDots[key]; + dots.forEach(dot => { + ctx.beginPath(); + ctx.arc(cellX * tileSize + dot.x, cellY * tileSize + dot.y, 2, 0, Math.PI * 2); + ctx.fillStyle = dot.color; + ctx.fill(); + ctx.closePath(); + }); + } + } + + function handleItemCollection() { + const collectedItem = items.find(item => item.x === player.x && item.y === player.y); + if (collectedItem) { + player.itemsCollected++; + if (collectedItem.type === 'diamond') { + player.damage += 3; + console.log("Collected diamond! +3 damage on this level."); + } else if (collectedItem.type === 'pentagon') { + const healAmount = Math.floor(Math.random() * 2) + 1; + player.health = Math.min(player.health + healAmount, PLAYER_MAX_HEALTH); + console.log("Collected pentagon! Healed " + healAmount + " health."); + } + items = items.filter(item => item !== collectedItem); // Remove collected item + } + } + + // Function to add dots for damage in combat (including adjacent cells) + function addCombatDots(x, y, color) { + const cellsToFill = [ + [x, y], // Center + [x - 1, y], // Left + [x + 1, y], // Right + [x, y - 1], // Up + [x, y + 1], // Down + [x - 1, y - 1], // Top-left + [x + 1, y - 1], // Top-right + [x - 1, y + 1], // Bottom-left + [x + 1, y + 1] // Bottom-right + ]; + + cellsToFill.forEach(([cellX, cellY]) => { + if (cellX >= 0 && cellX < GRID_SIZE && cellY >= 0 && cellY < GRID_SIZE) { + const key = `${cellX},${cellY}`; + if (!combatDots[key]) { + combatDots[key] = []; + } + for (let i = 0; i < DOTS_PER_HIT; i++) { + combatDots[key].push({ + x: Math.random() * tileSize, + y: Math.random() * tileSize, + color: color + }); + } + } + }); + } + + function movePlayer(dx, dy) { + const newX = player.x + dx; + const newY = player.y + dy; + if (isValidMove(newX, newY)) { + const enemyInTargetCell = enemies.find(enemy => enemy.x === newX && enemy.y === newY); + if (!enemyInTargetCell) { + if (newX !== player.x || newY !== player.y) player.cellsTraveled++; + player.x = newX; + player.y = newY; + handleItemCollection(); // Check if the player collected an item + checkPlayerAtExit(); // Check if player reached the exit after moving + } else { + // Enemy in the target cell, stay in place and do combat + handleDamage(player, enemyInTargetCell); + } + } + moveEnemies(); + render(); + } + + // Chase logic (naive) + function moveEnemies() { + enemies.forEach(enemy => { + const distance = Math.abs(enemy.x - player.x) + Math.abs(enemy.y - player.y); + if (distance <= ENEMY_CHASE_DISTANCE) { + let dx = 0, dy = 0; + if (enemy.x < player.x && isValidMove(enemy.x + 1, enemy.y)) dx = 1; + else if (enemy.x > player.x && isValidMove(enemy.x - 1, enemy.y)) dx = -1; + else if (enemy.y < player.y && isValidMove(enemy.x, enemy.y + 1)) dy = 1; + else if (enemy.y > player.x && isValidMove(enemy.x, enemy.y - 1)) dy = -1; + + if (!enemies.some(e => e.x === enemy.x + dx && e.y === enemy.y)) { + enemy.x += dx; + enemy.y += dy; + } + } + }); + } + + function handleDamage(player, enemy) { + const enemyMisses = Math.random() < 0.2; // 1 in 5 chance the enemy misses you + const cellX = player.x; + const cellY = player.y; + + if (!enemyMisses) { + player.health--; + player.totalDamageTaken++; + addCombatDots(cellX, cellY, COLORS.combatDotPlayer); // Add dots for player damage + console.log("Enemy hit! Player health: " + player.health); + } else { + console.log("Enemy missed!"); + } + + enemy.health = enemy.health - player.damage; + player.totalDamageDone++; + addCombatDots(cellX, cellY, COLORS.combatDotEnemy); // Add dots for enemy damage + console.log("Player hit! Enemy health: " + enemy.health); + + if (enemy.health <= 0) { + player.killCount++; + enemies = enemies.filter(e => e !== enemy); + } + + if (player.health <= 0) { + alert(`Dead\n\nScore: ${player.score}\nDistance Traveled: ${player.cellsTraveled}\nTotal Damage Dealt: ${player.totalDamageDone}\nTotal Damage Received: ${player.totalDamageTaken}\nCircles Vanquished: ${player.killCount}\nItems Collected: ${player.itemsCollected}`); + resetGame(); + } + } + + function resetGame() { + player.health = PLAYER_HEALTH; + player.damage = PLAYER_BASE_DAMAGE; + player.bonusDamageTurns = 0; + player.totalDamageDone = 0; + player.totalDamageTaken = 0; + player.cellsTraveled = 0; + player.score = 0; + player.killCount = 0; + player.itemsCollected = 0; + player.x = 0; + player.y = 0; + combatDots = {}; + generateExit(); + generateEnemies(); + generateItems(); + generateWalls(); + render(); + } + + function checkPlayerAtExit() { + if (player.x === exit.x && player.y === exit.y) { + player.score += 1; + player.damage = PLAYER_BASE_DAMAGE; + console.group("Level complete! " + player.score); + console.log("Score: " + player.score); + console.log("Current health: " + player.health); + console.log("Distance Traveled: " + player.cellsTraveled); + console.log("Total Damage Dealt: " + player.totalDamageDone); + console.log("Total Damage Received: " + player.totalDamageTaken); + console.log("Circles Vanquished: " + player.killCount); + console.log("Items Collected: " + player.itemsCollected); + console.groupEnd(); + combatDots = {}; + generateExit(); + generateEnemies(); + generateItems(); + generateWalls(); + render(); + } + } + + function render() { + drawGrid(); + drawExit(); + drawItems(); + drawEnemies(); + drawPlayer(); + drawWalls(); + drawCombatDots(); + } + + const directionMap = { + ArrowUp: [0, -1], + ArrowDown: [0, 1], + ArrowLeft: [-1, 0], + ArrowRight: [1, 0], + w: [0, -1], + s: [0, 1], + a: [-1, 0], + d: [1, 0], + h: [-1, 0], + j: [0, 1], + k: [0, -1], + l: [1, 0] + }; + + document.addEventListener('keydown', (e) => { + const direction = directionMap[e.key]; + if (direction) { + movePlayer(...direction); + checkPlayerAtExit(); + render(); + } + }); + + let touchStartX = 0; + let touchStartY = 0; + + canvas.addEventListener('touchstart', (e) => { + e.preventDefault(); // Prevent scrolling on touchstart + touchStartX = e.touches[0].clientX; + touchStartY = e.touches[0].clientY; + }); + + canvas.addEventListener('touchend', (e) => { + e.preventDefault(); // Prevent scrolling on touchend + const touchEndX = e.changedTouches[0].clientX; + const touchEndY = e.changedTouches[0].clientY; + const dx = touchEndX - touchStartX; + const dy = touchEndY - touchStartY; + + if (Math.abs(dx) > Math.abs(dy)) { + // Horizontal swipe + if (dx > 0) { + movePlayer(1, 0); // Swipe right + } else { + movePlayer(-1, 0); // Swipe left + } + } else { + // Vertical swipe + if (dy > 0) { + movePlayer(0, 1); // Swipe down + } else { + movePlayer(0, -1); // Swipe up + } + } + + render(); + }, { passive: false }); // TIL you can use passive set to false to help make preventDefault actually work? Feels like superstition + + const resizeCanvas = () => { + const rect = canvas.getBoundingClientRect(); + canvas.width = rect.width; + canvas.height = rect.height; + tileSize = canvas.width / GRID_SIZE; // Update tile size based on canvas dimensions + render(); + }; + + window.addEventListener('resize', resizeCanvas); + resizeCanvas(); + + // Initial level setup + generateExit(); + generateEnemies(); + generateItems(); + generateWalls(); + render(); + </script> +</body> +</html> \ No newline at end of file diff --git a/html/file-system/index.html b/html/file-system/index.html new file mode 100644 index 0000000..89e05b8 --- /dev/null +++ b/html/file-system/index.html @@ -0,0 +1,297 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Folders in Folders in Folders</title> + <style> + body { + font-family: Arial, sans-serif; + background-color: beige; + display: flex; + flex-direction: column; + align-items: center; + } + #navigation { + display: flex; + flex-wrap: wrap; + justify-content: center; + margin: 0.625em 0; + } + #navigation button { + margin: 0.3125em; + padding: 0.625em 0.9375em; + font-size: 1em; + flex-grow: 1; + min-width: 7.5em; + } + #currentPath { + font-weight: bold; + margin: 0.625em 0; + } + ul { + list-style-type: none; + width: 100%; + max-width: 37.5em; + } + li { + margin: 0.3125em 0; + cursor: pointer; + } + .folder { + display: inline-block; + padding: 0.3125em 0; + cursor: pointer; + font-size: 1.125em; + color: black; + } + #editor { + margin-top: 1.25em; + width: 100%; + max-width: 37.5em; + } + #editor textarea { + width: 100%; + height: 9.375em; + font-size: 1em; + padding: 0.625em; + box-sizing: border-box; + resize: none; + } + h1 { + text-align: center; + font-size: 1.5em; + margin-top: 0.625em; + } + @media (max-width: 37.5em) { + h1 { + font-size: 1.25em; + } + #navigation button { + font-size: 0.875em; + padding: 0.5em 0.625em; + } + .folder { + font-size: 1em; + } + #editor textarea { + height: 7.5em; + font-size: 0.875em; + } + } + </style> +</head> +<body> + +<div id="navigation"> + <button onclick="goToRoot()">Go to Root</button> + <button onclick="goUp()">Go Up One Level</button> + <button onclick="createFolderPrompt()">Create Folder</button> + <button onclick="copyItem()">Copy</button> + <button onclick="pasteItem()">Paste</button> + <button onclick="deleteItem()">Delete</button> +</div> + +<h1 id="currentPath">Current Folder: /</h1> + +<ul id="fileSystemTree"></ul> + +<div id="editor"> + <h2>Folder Content Editor</h2> + <textarea id="folderContent" placeholder="Select a folder to edit its contents"></textarea> +</div> + +<script> +const initialState = () => ({ + root: { type: 'folder', children: {}, content: '' } +}); + +const loadFileSystem = () => JSON.parse(localStorage.getItem('fileSystem')) || initialState(); + +const state = { + fileSystem: loadFileSystem(), + currentFolderPath: 'root', + copiedFolderData: null, // Pastebin for the copied folder + copiedItemPath: null +}; + +const clone = (obj) => JSON.parse(JSON.stringify(obj)); + +const updateFileSystem = (newFileSystem) => { + state.fileSystem = newFileSystem; + saveFileSystem(newFileSystem); +}; + +const getFolder = (path, fileSystem = state.fileSystem) => { + const parts = path.split('/').filter(part => part !== 'root'); + return parts.reduce((current, part) => { + if (!current || !current.children[part]) { + return null; + } + return current.children[part]; + }, fileSystem.root); +}; + +const updateFolder = (fileSystem, path, updateFn) => { + const rootCopy = clone(fileSystem); + const folderToUpdate = getFolder(path, rootCopy); + if (!folderToUpdate) { + alert(`Folder not found for path: ${path}`); + return null; + } + + updateFn(folderToUpdate); + return rootCopy; +}; + +const addFolder = (path, name, folderData, fileSystem) => { + return updateFolder(fileSystem, path, (folder) => { + if (folder.children[name]) { + alert(`Folder with name "${name}" already exists.`); + return; + } + folder.children[name] = folderData || { type: 'folder', children: {}, content: '' }; + }); +}; + +const deleteFolder = (path, fileSystem) => { + const [parentPath, folderName] = path.split(/\/([^\/]+)$/); + return updateFolder(fileSystem, parentPath, (folder) => { + delete folder.children[folderName]; + }); +}; + +const saveFileSystem = (fileSystem) => { + localStorage.setItem('fileSystem', JSON.stringify(fileSystem)); +}; + +const goToRoot = () => { + state.currentFolderPath = 'root'; + render(); +}; + +const goUp = () => { + const parts = state.currentFolderPath.split('/'); + if (parts.length > 1) { + parts.pop(); + state.currentFolderPath = parts.join('/'); + render(); + } +}; + +const createFolder = (path, name) => { + const updatedFileSystem = addFolder(path, name, null, state.fileSystem); + if (updatedFileSystem) { + updateFileSystem(updatedFileSystem); + render(); + } else { + console.error('Error creating folder', path, name); + alert('Error creating folder'); + } +}; + +const deleteItem = () => { + if (state.currentFolderPath === 'root') return alert("Can't delete the root!"); + + const folderToDelete = getFolder(state.currentFolderPath); + if (!folderToDelete) return alert("Folder not found!"); + + const hasNestedFolders = Object.keys(folderToDelete.children).length > 0; + + if (hasNestedFolders) { + const confirmDelete = confirm(`The folder contains nested folders. Do you really wanna delete them too?`); + if (!confirmDelete) { + return; + } + } + + const updatedFileSystem = deleteFolder(state.currentFolderPath, state.fileSystem); + if (updatedFileSystem) { + const parts = state.currentFolderPath.split('/'); + parts.pop(); + state.currentFolderPath = parts.join('/') || 'root'; + updateFileSystem(updatedFileSystem); + render(); + } +}; + +const copyItem = () => { + const folder = getFolder(state.currentFolderPath); + if (folder) { + state.copiedFolderData = clone(folder); // Store a deep copy of the folder + state.copiedItemPath = state.currentFolderPath; // Store the path so that we can remove later + // console.log('Folder copied:', state.copiedFolderData); + } +}; + +const pasteItem = () => { + if (!state.copiedItemPath || !state.copiedFolderData) return alert('No item to paste'); + + const [oldPath, folderName] = state.copiedItemPath.split(/\/([^\/]+)$/); + const updatedFileSystem = addFolder(state.currentFolderPath, folderName, state.copiedFolderData, state.fileSystem); + + if (updatedFileSystem) { + updateFileSystem(deleteFolder(oldPath, updatedFileSystem)); // Remove original + state.copiedFolderData = null; // Clear the copied data + state.copiedItemPath = null; + render(); + } else { + alert('Error pasting item'); + } +}; + +const saveFolderContent = () => { + const updatedFileSystem = updateFolder(state.fileSystem, state.currentFolderPath, (folder) => { + folder.content = document.getElementById('folderContent').value; + }); + if (updatedFileSystem) { + updateFileSystem(updatedFileSystem); + } +}; + +document.getElementById('folderContent').addEventListener('input', () => { + saveFolderContent(); +}); + +const renderFolderTree = (folder, path = 'root') => { + const entries = Object.entries(folder.children); + return entries.length ? entries.map(([name, item]) => ` + <li> + <span class="folder" onclick="selectFolder('${path}/${name}')">${name}</span> + <ul>${renderFolderTree(item, `${path}/${name}`)}</ul> + </li> + `).join('') : ''; +}; + +const selectFolder = (path) => { + const folder = getFolder(path); + if (folder) { + state.currentFolderPath = path; + document.getElementById('folderContent').value = folder.content || ''; + render(); + } else { + console.error('Folder not found', path); + alert('Folder not found'); + } +}; + +const render = () => { + if (!state.fileSystem.root) { + console.error('File system is not initialized correctly', state.fileSystem); + alert('File system is not initialized correctly'); + return; + } + document.getElementById('currentPath').textContent = state.currentFolderPath.replace('root', '') || '/'; + document.getElementById('fileSystemTree').innerHTML = renderFolderTree(state.fileSystem.root); +}; + +const createFolderPrompt = () => { + const name = prompt('Enter folder name:'); + if (name) createFolder(state.currentFolderPath, name); +}; + +render(); +</script> + +</body> +</html> \ No newline at end of file diff --git a/html/playground/APL386.ttf b/html/playground/APL386.ttf new file mode 100644 index 0000000..5e3a338 --- /dev/null +++ b/html/playground/APL386.ttf Binary files differdiff --git a/html/playground/index.html b/html/playground/index.html new file mode 100644 index 0000000..680f022 --- /dev/null +++ b/html/playground/index.html @@ -0,0 +1,243 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>JavaScript Playground</title> + <meta name="description" content="A JavaScript jungle-gym for doing experiments and sharing scrappy fiddles."> + <style> + body { + display: flex; + flex-direction: column; + align-items: center; + background-color: #ddd; + padding: 10px; + height: 100vh; + margin: 0; + } + + textarea { + width: 100%; + height: 64%; + font-family: monospace; + background-color: #FFFEEC; + border: 2px solid #000; + scrollbar-width: none; + font-size: 1rem; + margin-bottom: 10px; + padding: 10px; + box-sizing: border-box; + resize: none; + border-bottom: 12px solid teal; + -webkit-user-modify: read-write-plaintext-only; + } + + textarea::-webkit-scrollbar { + display: none; + } + + textarea::selection { + background-color: #EFECA7; + } + + textarea:focus { + outline: none; + } + + #console { + width: 100%; + height: 22%; + background-color: #000; + color: #0fc; + font-family: monospace; + font-size: 1rem; + overflow-y: auto; + padding: 10px; + box-sizing: border-box; + white-space: pre-wrap; + } + + .button-container { + width: 100%; + display: flex; + justify-content: flex-end; + margin-bottom: 10px; + } + + button { + padding: 10px 20px; + font-size: 1rem; + margin-left: 10px; + cursor: pointer; + border: none; + transition: background-color 0.3s ease; + } + + button:hover, button:focus { + outline: none; + } + + button.run { + background-color: teal; + color: #FFFFFF; + } + </style> +</head> +<body> + + <div class="playground" id="playground"> + <div class="seesaw" id="seesaw"></div> + <div class="slide" id="slide"></div> + </div> + + <div class="button-container"> + <button onclick="clearEverything()">Clear</button> + <button onclick="downloadCodeAndEditor()">Download</button> + <button onclick="shareCode()">Share</button> + <button onclick="evaluateCode()" class="run">Run Code</button> + </div> + <textarea id="codeInput" placeholder="Enter JavaScript..." spellcheck="false"></textarea> + <div id="console"></div> + + <script> + function evaluateCode() { + const code = document.getElementById('codeInput').value; + const consoleDiv = document.getElementById('console'); + consoleDiv.innerHTML = ''; + + // Custom console.log function to output to the console div + const originalConsoleLog = console.log; + console.log = function(...args) { + args.forEach(arg => { + const output = document.createElement('div'); + output.textContent = typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg; + consoleDiv.appendChild(output); + }); + originalConsoleLog.apply(console, args); + }; + + try { + eval(code); + } catch (error) { + const errorOutput = document.createElement('div'); + errorOutput.textContent = error; + errorOutput.style.color = 'red'; + consoleDiv.appendChild(errorOutput); + } + + // Restore browser's console.log + console.log = originalConsoleLog; + } + + function downloadCodeAndEditor() { + const codeInput = document.getElementById('codeInput').value; + const htmlContent = document.documentElement.outerHTML.replace( + /<textarea id="codeInput"[^>]*>.*<\/textarea>/, + `<textarea id="codeInput">${codeInput}</textarea>` + ); + + const blob = new Blob([htmlContent], { type: 'text/html' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'code_editor.html'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } + + function shareCode() { + const code = document.getElementById('codeInput').value; + const encodedCode = btoa(encodeURIComponent(code)); + window.location.hash = encodedCode; + window.prompt("Copy the URL to share.\nBe warned! Very long URLs don't share wicked well, sometimes.", window.location.href); + } + + function clearEverything() { + if (!confirm('Are you sure you want to reset the playground?')) { + return; + } else { + window.location.hash = ''; + window.location.reload(); + } + } + + function loadCodeFromHash() { + const hash = window.location.hash.substring(1); + if (hash) { + try { + const decodedCode = decodeURIComponent(atob(hash)); + document.getElementById('codeInput').value = decodedCode; + } catch (error) { + console.error('Failed to decode the URL hash:', error); + } + } + } + + function help() { + const helpText = ` + Welcome to the JavaScript Playground! Here are some tips to get you started: + + 1. Enter your JavaScript code in the textarea. + 2. Click the "Run Code" button to execute your code. + 3. The console output will be displayed below the textarea. + 4. Click the "Clear" button to reset the playground. + 5. Click the "Download" button to save your code and editor as an HTML file. + 6. Click the "Share" button to generate a URL to share your code with others. + 7. You can also press "Cmd + Enter" to run your code. + 8. There's an empty div above the buttons with the id "playground" + 9. You can mount stuff to it using the "mount" function, for more info run "mountHelp()" + 10. You can use the "clear()" function to clear the content's of the console. + + Go nuts! Share scrappy fiddles! + `; + console.log(helpText); + } + + function clear() { + document.getElementById('console').innerHTML = ''; + } + + function mountHelp() { + console.log(` + The mount function is used to mount stuff to the playground div. + It takes a function as an argument, which in turn receives the playground div as an argument. + Before mounting, it clears the playground div. + Here's an example of how to use the mount function: + + mount(playground => { + const h1 = document.createElement('h1'); + h1.textContent = 'Hell is empty and all the devils are here.'; + playground.appendChild(h1); + }); + + This will add an h1 element to the playground div. + `); + } + + function mount(mountFunction) { + const playground = document.getElementById('playground'); + if (!playground) { + console.error("Couldn't find a div with the id 'playground'! You may need to reload the page."); + return; + } + + if (playground.innerHTML.trim() !== "") { + playground.innerHTML = ""; + } + mountFunction(playground); + } + + + document.getElementById('codeInput').addEventListener('keydown', function(event) { + if (event.metaKey && event.key === 'Enter') { + event.preventDefault(); + evaluateCode(); + } + }); + + window.onload = loadCodeFromHash; + </script> +</body> +</html> diff --git a/html/playground/little-regex.html b/html/playground/little-regex.html new file mode 100644 index 0000000..c09139f --- /dev/null +++ b/html/playground/little-regex.html @@ -0,0 +1,728 @@ +<html lang="en"><head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>JavaScript Playground</title> + <meta name="description" content="A JavaScript jungle-gym for doing experiments and sharing scrappy fiddles."> + <style> + body { + display: flex; + flex-direction: column; + align-items: center; + background-color: #ddd; + padding: 10px; + height: 100vh; + margin: 0; + } + + textarea { + width: 100%; + height: 64%; + font-family: monospace; + background-color: #FFFEEC; + border: 2px solid #000; + scrollbar-width: none; + font-size: 1rem; + margin-bottom: 10px; + padding: 10px; + box-sizing: border-box; + resize: none; + border-bottom: 12px solid teal; + -webkit-user-modify: read-write-plaintext-only; + } + + textarea::-webkit-scrollbar { + display: none; + } + + textarea::selection { + background-color: #EFECA7; + } + + textarea:focus { + outline: none; + } + + #console { + width: 100%; + height: 22%; + background-color: #000; + color: #0fc; + font-family: monospace; + font-size: 1rem; + overflow-y: auto; + padding: 10px; + box-sizing: border-box; + white-space: pre-wrap; + } + + .button-container { + width: 100%; + display: flex; + justify-content: flex-end; + margin-bottom: 10px; + } + + button { + padding: 10px 20px; + font-size: 1rem; + margin-left: 10px; + cursor: pointer; + border: none; + transition: background-color 0.3s ease; + } + + button:hover, button:focus { + outline: none; + } + + button.run { + background-color: teal; + color: #FFFFFF; + } + </style> +</head> +<body> + + <div class="playground" id="playground"> + <div class="seesaw" id="seesaw"></div> + <div class="slide" id="slide"></div> + </div> + + <div class="button-container"> + <button onclick="clearEverything()">Clear</button> + <button onclick="downloadCodeAndEditor()">Download</button> + <button onclick="shareCode()">Share</button> + <button onclick="evaluateCode()" class="run">Run Code</button> + </div> + <textarea id="codeInput">// Inspired by <https://www.cs.princeton.edu/courses/archive/spr09/cos333/beautiful.html> +const matchHere = (pattern, text) => { + if (pattern.length === 0) return true; + + // If pattern ends with $, match end of string + if (pattern[0] === ' + <div id="console"></div> + + <script> + function evaluateCode() { + const code = document.getElementById('codeInput').value; + const consoleDiv = document.getElementById('console'); + consoleDiv.innerHTML = ''; + + // Custom console.log function to output to the console div + const originalConsoleLog = console.log; + console.log = function(...args) { + args.forEach(arg => { + const output = document.createElement('div'); + output.textContent = typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg; + consoleDiv.appendChild(output); + }); + originalConsoleLog.apply(console, args); + }; + + try { + eval(code); + } catch (error) { + const errorOutput = document.createElement('div'); + errorOutput.textContent = error; + errorOutput.style.color = 'red'; + consoleDiv.appendChild(errorOutput); + } + + // Restore browser's console.log + console.log = originalConsoleLog; + } + + function downloadCodeAndEditor() { + const codeInput = document.getElementById('codeInput').value; + const htmlContent = document.documentElement.outerHTML.replace( + /<textarea id="codeInput"[^>]*>.*<\/textarea>/, + `<textarea id="codeInput">${codeInput}</textarea>` + ); + + const blob = new Blob([htmlContent], { type: 'text/html' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'code_editor.html'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } + + function shareCode() { + const code = document.getElementById('codeInput').value; + const encodedCode = btoa(encodeURIComponent(code)); + window.location.hash = encodedCode; + window.prompt("Copy the URL to share.\nBe warned! Very long URLs don't share wicked well, sometimes.", window.location.href); + } + + function clearEverything() { + if (!confirm('Are you sure you want to reset the playground?')) { + return; + } else { + window.location.hash = ''; + window.location.reload(); + } + } + + function loadCodeFromHash() { + const hash = window.location.hash.substring(1); + if (hash) { + try { + const decodedCode = decodeURIComponent(atob(hash)); + document.getElementById('codeInput').value = decodedCode; + } catch (error) { + console.error('Failed to decode the URL hash:', error); + } + } + } + + function help() { + const helpText = ` + Welcome to the JavaScript Playground! Here are some tips to get you started: + + 1. Enter your JavaScript code in the textarea. + 2. Click the "Run Code" button to execute your code. + 3. The console output will be displayed below the textarea. + 4. Click the "Clear" button to reset the playground. + 5. Click the "Download" button to save your code and editor as an HTML file. + 6. Click the "Share" button to generate a URL to share your code with others. + 7. You can also press "Cmd + Enter" to run your code. + 8. There's an empty div above the buttons with the id "playground" + 9. You can mount stuff to it using the "mount" function, for more info run "mountHelp()" + 10. You can use the "clear()" function to clear the content's of the console. + + Go nuts! Share scrappy fiddles! + `; + console.log(helpText); + } + + function clear() { + document.getElementById('console').innerHTML = ''; + } + + function mountHelp() { + console.log(` + The mount function is used to mount stuff to the playground div. + It takes a function as an argument, which in turn receives the playground div as an argument. + Before mounting, it clears the playground div. + Here's an example of how to use the mount function: + + mount(playground => { + const h1 = document.createElement('h1'); + h1.textContent = 'Hell is empty and all the devils are here.'; + playground.appendChild(h1); + }); + + This will add an h1 element to the playground div. + `); + } + + function mount(mountFunction) { + const playground = document.getElementById('playground'); + if (!playground) { + console.error("Couldn't find a div with the id 'playground'! You may need to reload the page."); + return; + } + + if (playground.innerHTML.trim() !== "") { + playground.innerHTML = ""; + } + mountFunction(playground); + } + + + document.getElementById('codeInput').addEventListener('keydown', function(event) { + if (event.metaKey && event.key === 'Enter') { + event.preventDefault(); + evaluateCode(); + } + }); + + window.onload = loadCodeFromHash; + </script> + + +</body></html> && pattern.length === 1) return text.length === 0; + + // If next char is '*', match zero or more occurrences of prev char + if (pattern[1] === '*') return matchStar(pattern[0], pattern.slice(2), text); + + // Match . or literal character + if (text.length !== 0 && (pattern[0] === '.' || pattern[0] === text[0])) { + return matchHere(pattern.slice(1), text.slice(1)); + } + + return false; +}; + +const matchStar = (prevChar, pattern, text) => { + // Try matching zero occurrences first + if (matchHere(pattern, text)) return true; + + // Then, match one or more occurrences of prevChar + while (text.length > 0 && (text[0] === prevChar || prevChar === '.')) { + text = text.slice(1); + if (matchHere(pattern, text)) return true; + } + + return false; +}; + +const match = (pattern, text) => { + // Handle ^ anchor at the beginning + if (pattern[0] === '^') { + return matchHere(pattern.slice(1), text); + } + + // Otherwise, check the entire string + for (let i = 0; i <= text.length; i++) { + if (matchHere(pattern, text.slice(i))) return true; + } + + return false; +}; + +const assertEqual = (actual, expected, testName) => + console.log(actual === expected ? `Passed: ${testName}` : `Failed: ${testName}. Expected ${expected} but got ${actual}`); + +assertEqual(match("ab*c", "ac"), true, "Star match 'ab*c' vs 'ac' (zero occurrences)"); +assertEqual(match("ab*c", "abbbc"), true, "Star match 'ab*c' vs 'abbbc' (multiple occurrences)"); +assertEqual(match("^ab.*c$", "abc"), true, "Complex match '^ab.*c + <div id="console"></div> + + <script> + function evaluateCode() { + const code = document.getElementById('codeInput').value; + const consoleDiv = document.getElementById('console'); + consoleDiv.innerHTML = ''; + + // Custom console.log function to output to the console div + const originalConsoleLog = console.log; + console.log = function(...args) { + args.forEach(arg => { + const output = document.createElement('div'); + output.textContent = typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg; + consoleDiv.appendChild(output); + }); + originalConsoleLog.apply(console, args); + }; + + try { + eval(code); + } catch (error) { + const errorOutput = document.createElement('div'); + errorOutput.textContent = error; + errorOutput.style.color = 'red'; + consoleDiv.appendChild(errorOutput); + } + + // Restore browser's console.log + console.log = originalConsoleLog; + } + + function downloadCodeAndEditor() { + const codeInput = document.getElementById('codeInput').value; + const htmlContent = document.documentElement.outerHTML.replace( + /<textarea id="codeInput"[^>]*>.*<\/textarea>/, + `<textarea id="codeInput">${codeInput}</textarea>` + ); + + const blob = new Blob([htmlContent], { type: 'text/html' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'code_editor.html'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } + + function shareCode() { + const code = document.getElementById('codeInput').value; + const encodedCode = btoa(encodeURIComponent(code)); + window.location.hash = encodedCode; + window.prompt("Copy the URL to share.\nBe warned! Very long URLs don't share wicked well, sometimes.", window.location.href); + } + + function clearEverything() { + if (!confirm('Are you sure you want to reset the playground?')) { + return; + } else { + window.location.hash = ''; + window.location.reload(); + } + } + + function loadCodeFromHash() { + const hash = window.location.hash.substring(1); + if (hash) { + try { + const decodedCode = decodeURIComponent(atob(hash)); + document.getElementById('codeInput').value = decodedCode; + } catch (error) { + console.error('Failed to decode the URL hash:', error); + } + } + } + + function help() { + const helpText = ` + Welcome to the JavaScript Playground! Here are some tips to get you started: + + 1. Enter your JavaScript code in the textarea. + 2. Click the "Run Code" button to execute your code. + 3. The console output will be displayed below the textarea. + 4. Click the "Clear" button to reset the playground. + 5. Click the "Download" button to save your code and editor as an HTML file. + 6. Click the "Share" button to generate a URL to share your code with others. + 7. You can also press "Cmd + Enter" to run your code. + 8. There's an empty div above the buttons with the id "playground" + 9. You can mount stuff to it using the "mount" function, for more info run "mountHelp()" + 10. You can use the "clear()" function to clear the content's of the console. + + Go nuts! Share scrappy fiddles! + `; + console.log(helpText); + } + + function clear() { + document.getElementById('console').innerHTML = ''; + } + + function mountHelp() { + console.log(` + The mount function is used to mount stuff to the playground div. + It takes a function as an argument, which in turn receives the playground div as an argument. + Before mounting, it clears the playground div. + Here's an example of how to use the mount function: + + mount(playground => { + const h1 = document.createElement('h1'); + h1.textContent = 'Hell is empty and all the devils are here.'; + playground.appendChild(h1); + }); + + This will add an h1 element to the playground div. + `); + } + + function mount(mountFunction) { + const playground = document.getElementById('playground'); + if (!playground) { + console.error("Couldn't find a div with the id 'playground'! You may need to reload the page."); + return; + } + + if (playground.innerHTML.trim() !== "") { + playground.innerHTML = ""; + } + mountFunction(playground); + } + + + document.getElementById('codeInput').addEventListener('keydown', function(event) { + if (event.metaKey && event.key === 'Enter') { + event.preventDefault(); + evaluateCode(); + } + }); + + window.onload = loadCodeFromHash; + </script> + + +</body></html> vs 'abc'"); +assertEqual(match("^ab.*c$", "abcd"), false, "Complex mismatch '^ab.*c + <div id="console"></div> + + <script> + function evaluateCode() { + const code = document.getElementById('codeInput').value; + const consoleDiv = document.getElementById('console'); + consoleDiv.innerHTML = ''; + + // Custom console.log function to output to the console div + const originalConsoleLog = console.log; + console.log = function(...args) { + args.forEach(arg => { + const output = document.createElement('div'); + output.textContent = typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg; + consoleDiv.appendChild(output); + }); + originalConsoleLog.apply(console, args); + }; + + try { + eval(code); + } catch (error) { + const errorOutput = document.createElement('div'); + errorOutput.textContent = error; + errorOutput.style.color = 'red'; + consoleDiv.appendChild(errorOutput); + } + + // Restore browser's console.log + console.log = originalConsoleLog; + } + + function downloadCodeAndEditor() { + const codeInput = document.getElementById('codeInput').value; + const htmlContent = document.documentElement.outerHTML.replace( + /<textarea id="codeInput"[^>]*>.*<\/textarea>/, + `<textarea id="codeInput">${codeInput}</textarea>` + ); + + const blob = new Blob([htmlContent], { type: 'text/html' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'code_editor.html'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } + + function shareCode() { + const code = document.getElementById('codeInput').value; + const encodedCode = btoa(encodeURIComponent(code)); + window.location.hash = encodedCode; + window.prompt("Copy the URL to share.\nBe warned! Very long URLs don't share wicked well, sometimes.", window.location.href); + } + + function clearEverything() { + if (!confirm('Are you sure you want to reset the playground?')) { + return; + } else { + window.location.hash = ''; + window.location.reload(); + } + } + + function loadCodeFromHash() { + const hash = window.location.hash.substring(1); + if (hash) { + try { + const decodedCode = decodeURIComponent(atob(hash)); + document.getElementById('codeInput').value = decodedCode; + } catch (error) { + console.error('Failed to decode the URL hash:', error); + } + } + } + + function help() { + const helpText = ` + Welcome to the JavaScript Playground! Here are some tips to get you started: + + 1. Enter your JavaScript code in the textarea. + 2. Click the "Run Code" button to execute your code. + 3. The console output will be displayed below the textarea. + 4. Click the "Clear" button to reset the playground. + 5. Click the "Download" button to save your code and editor as an HTML file. + 6. Click the "Share" button to generate a URL to share your code with others. + 7. You can also press "Cmd + Enter" to run your code. + 8. There's an empty div above the buttons with the id "playground" + 9. You can mount stuff to it using the "mount" function, for more info run "mountHelp()" + 10. You can use the "clear()" function to clear the content's of the console. + + Go nuts! Share scrappy fiddles! + `; + console.log(helpText); + } + + function clear() { + document.getElementById('console').innerHTML = ''; + } + + function mountHelp() { + console.log(` + The mount function is used to mount stuff to the playground div. + It takes a function as an argument, which in turn receives the playground div as an argument. + Before mounting, it clears the playground div. + Here's an example of how to use the mount function: + + mount(playground => { + const h1 = document.createElement('h1'); + h1.textContent = 'Hell is empty and all the devils are here.'; + playground.appendChild(h1); + }); + + This will add an h1 element to the playground div. + `); + } + + function mount(mountFunction) { + const playground = document.getElementById('playground'); + if (!playground) { + console.error("Couldn't find a div with the id 'playground'! You may need to reload the page."); + return; + } + + if (playground.innerHTML.trim() !== "") { + playground.innerHTML = ""; + } + mountFunction(playground); + } + + + document.getElementById('codeInput').addEventListener('keydown', function(event) { + if (event.metaKey && event.key === 'Enter') { + event.preventDefault(); + evaluateCode(); + } + }); + + window.onload = loadCodeFromHash; + </script> + + +</body></html> vs 'abcd'");</textarea> + <div id="console"></div> + + <script> + function evaluateCode() { + const code = document.getElementById('codeInput').value; + const consoleDiv = document.getElementById('console'); + consoleDiv.innerHTML = ''; + + // Custom console.log function to output to the console div + const originalConsoleLog = console.log; + console.log = function(...args) { + args.forEach(arg => { + const output = document.createElement('div'); + output.textContent = typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg; + consoleDiv.appendChild(output); + }); + originalConsoleLog.apply(console, args); + }; + + try { + eval(code); + } catch (error) { + const errorOutput = document.createElement('div'); + errorOutput.textContent = error; + errorOutput.style.color = 'red'; + consoleDiv.appendChild(errorOutput); + } + + // Restore browser's console.log + console.log = originalConsoleLog; + } + + function downloadCodeAndEditor() { + const codeInput = document.getElementById('codeInput').value; + const htmlContent = document.documentElement.outerHTML.replace( + /<textarea id="codeInput"[^>]*>.*<\/textarea>/, + `<textarea id="codeInput">${codeInput}</textarea>` + ); + + const blob = new Blob([htmlContent], { type: 'text/html' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'code_editor.html'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } + + function shareCode() { + const code = document.getElementById('codeInput').value; + const encodedCode = btoa(encodeURIComponent(code)); + window.location.hash = encodedCode; + window.prompt("Copy the URL to share.\nBe warned! Very long URLs don't share wicked well, sometimes.", window.location.href); + } + + function clearEverything() { + if (!confirm('Are you sure you want to reset the playground?')) { + return; + } else { + window.location.hash = ''; + window.location.reload(); + } + } + + function loadCodeFromHash() { + const hash = window.location.hash.substring(1); + if (hash) { + try { + const decodedCode = decodeURIComponent(atob(hash)); + document.getElementById('codeInput').value = decodedCode; + } catch (error) { + console.error('Failed to decode the URL hash:', error); + } + } + } + + function help() { + const helpText = ` + Welcome to the JavaScript Playground! Here are some tips to get you started: + + 1. Enter your JavaScript code in the textarea. + 2. Click the "Run Code" button to execute your code. + 3. The console output will be displayed below the textarea. + 4. Click the "Clear" button to reset the playground. + 5. Click the "Download" button to save your code and editor as an HTML file. + 6. Click the "Share" button to generate a URL to share your code with others. + 7. You can also press "Cmd + Enter" to run your code. + 8. There's an empty div above the buttons with the id "playground" + 9. You can mount stuff to it using the "mount" function, for more info run "mountHelp()" + 10. You can use the "clear()" function to clear the content's of the console. + + Go nuts! Share scrappy fiddles! + `; + console.log(helpText); + } + + function clear() { + document.getElementById('console').innerHTML = ''; + } + + function mountHelp() { + console.log(` + The mount function is used to mount stuff to the playground div. + It takes a function as an argument, which in turn receives the playground div as an argument. + Before mounting, it clears the playground div. + Here's an example of how to use the mount function: + + mount(playground => { + const h1 = document.createElement('h1'); + h1.textContent = 'Hell is empty and all the devils are here.'; + playground.appendChild(h1); + }); + + This will add an h1 element to the playground div. + `); + } + + function mount(mountFunction) { + const playground = document.getElementById('playground'); + if (!playground) { + console.error("Couldn't find a div with the id 'playground'! You may need to reload the page."); + return; + } + + if (playground.innerHTML.trim() !== "") { + playground.innerHTML = ""; + } + mountFunction(playground); + } + + + document.getElementById('codeInput').addEventListener('keydown', function(event) { + if (event.metaKey && event.key === 'Enter') { + event.preventDefault(); + evaluateCode(); + } + }); + + window.onload = loadCodeFromHash; + </script> + + +</body></html> \ No newline at end of file diff --git a/html/playground/regex.html b/html/playground/regex.html new file mode 100644 index 0000000..41f50e9 --- /dev/null +++ b/html/playground/regex.html @@ -0,0 +1,477 @@ +<html lang="en"><head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>JavaScript Playground</title> + <meta name="description" content="A JavaScript jungle-gym for doing experiments and sharing scrappy fiddles."> + <style> + body { + display: flex; + flex-direction: column; + align-items: center; + background-color: #ddd; + padding: 10px; + height: 100vh; + margin: 0; + } + + textarea { + width: 100%; + height: 64%; + font-family: monospace; + background-color: #FFFEEC; + border: 2px solid #000; + scrollbar-width: none; + font-size: 1rem; + margin-bottom: 10px; + padding: 10px; + box-sizing: border-box; + resize: none; + border-bottom: 12px solid teal; + -webkit-user-modify: read-write-plaintext-only; + } + + textarea::-webkit-scrollbar { + display: none; + } + + textarea::selection { + background-color: #EFECA7; + } + + textarea:focus { + outline: none; + } + + #console { + width: 100%; + height: 22%; + background-color: #000; + color: #0fc; + font-family: monospace; + font-size: 1rem; + overflow-y: auto; + padding: 10px; + box-sizing: border-box; + white-space: pre-wrap; + } + + .button-container { + width: 100%; + display: flex; + justify-content: flex-end; + margin-bottom: 10px; + } + + button { + padding: 10px 20px; + font-size: 1rem; + margin-left: 10px; + cursor: pointer; + border: none; + transition: background-color 0.3s ease; + } + + button:hover, button:focus { + outline: none; + } + + button.run { + background-color: teal; + color: #FFFFFF; + } + </style> +</head> +<body> + + <div class="playground" id="playground"> + <div class="seesaw" id="seesaw"></div> + <div class="slide" id="slide"></div> + </div> + + <div class="button-container"> + <button onclick="clearEverything()">Clear</button> + <button onclick="downloadCodeAndEditor()">Download</button> + <button onclick="shareCode()">Share</button> + <button onclick="evaluateCode()" class="run">Run Code</button> + </div> + <textarea id="codeInput">const tokenize = (pattern) => { + const tokens = []; + let i = 0; + + while (i < pattern.length) { + const char = pattern[i]; + + if (char === '.' || char === '*' || char === '(' || char === ')' || char === '|') { + tokens.push({ + type: char, + value: char + }); + } else if (char === '\\') { // Handle escaped characters + i++; + tokens.push({ + type: 'literal', + value: pattern[i] + }); + } else if (char === '[') { // Handle character classes + let charClass = ''; + i++; + while (pattern[i] !== ']' && i < pattern.length) { + charClass += pattern[i]; + i++; + } + tokens.push({ + type: 'charClass', + value: charClass + }); + } else { + tokens.push({ + type: 'literal', + value: char + }); + } + i++; + } + + return tokens; +}; + + + + +const parse = (tokens) => { + let i = 0; + + const parseSequenceExpression = () => { + const node = { + type: 'sequence', + elements: [] + }; + + while (i < tokens.length) { + const token = tokens[i]; + + if (token.type === 'literal') { + node.elements.push({ + type: 'literal', + value: token.value + }); + } else if (token.type === '*') { + const lastElement = node.elements.pop(); + node.elements.push({ + type: 'star', + element: lastElement + }); + } else if (token.type === '.') { + node.elements.push({ + type: 'dot' + }); + } else if (token.type === 'charClass') { + node.elements.push({ + type: 'charClass', + value: token.value + }); + } else if (token.type === '|') { + i++; + const right = parseSequenceExpression(); + return { + type: 'alternation', + left: node, + right + }; + } else if (token.type === '(') { + i++; + node.elements.push(parseSequenceExpression()); + } else if (token.type === ')') { + break; // End of a grouping + } + + i++; + } + + return node; + }; + + return parseSequenceExpression(); +}; + + + +const evaluateMatch = (node) => (input) => { + if (node.type === 'literal') { + return input[0] === node.value ? input.slice(1) : null; + } + if (node.type === 'dot') { + return input.length > 0 ? input.slice(1) : null; + } + if (node.type === 'star') { + let remainder = input; + while (remainder !== null) { + const next = evaluateMatch(node.element)(remainder); + if (next === null) { + break; + } + remainder = next; + } + return remainder; + } + if (node.type === 'charClass') { + return node.value.includes(input[0]) ? input.slice(1) : null; + } + if (node.type === 'alternation') { + const remainderLeft = evaluateMatch(node.left)(input); + const remainderRight = evaluateMatch(node.right)(input); + return remainderLeft !== null ? remainderLeft : remainderRight; + } + if (node.type === 'sequence') { + let remainder = input; + for (const element of node.elements) { + remainder = evaluateMatch(element)(remainder); + if (remainder === null) { + return null; + } + } + return remainder; + } +}; + + + + +const assertEqual = (expected, actual, message) => { + if (expected !== actual) { + console.error(`FAIL: ${message}`); + } else { + console.log(`PASS: ${message}`); + } +}; + + + +const runTests = () => { + const tests = [{ + pattern: "a.b*c", + input: "abbbc", + expected: true + }, + { + pattern: "a.b*c", + input: "abc", + expected: true + }, + { + pattern: "a.b*c", + input: "ac", + expected: true + }, + { + pattern: "a.b*c", + input: "abbc", + expected: true + }, + { + pattern: "a.b*c", + input: "axbc", + expected: false + }, + + // Character class tests + { + pattern: "[abc]x", + input: "bx", + expected: true + }, + { + pattern: "[abc]x", + input: "dx", + expected: false + }, + + // Grouping and alternation tests + { + pattern: "(a|b)c", + input: "ac", + expected: true + }, + { + pattern: "(a|b)c", + input: "bc", + expected: true + }, + { + pattern: "(a|b)c", + input: "cc", + expected: false + }, + + // Escaped characters tests + { + pattern: "a\\.b", + input: "a.b", + expected: true + }, + { + pattern: "a\\.b", + input: "a-b", + expected: false + }, + { + pattern: "a\\*b", + input: "a*b", + expected: true + } + ]; + + tests.forEach(({ pattern, input, expected }, index) => { + const tokens = tokenize(pattern); + const ast = parse(tokens); + const result = evaluateMatch(ast)(input) !== null; + assertEqual(expected, result, `Test ${index + 1}`); + }); +}; + +runTests();</textarea> + <div id="console"><div>PASS: Test 1</div><div>PASS: Test 2</div><div>PASS: Test 4</div><div>PASS: Test 6</div><div>PASS: Test 7</div><div>PASS: Test 8</div><div>PASS: Test 9</div><div>PASS: Test 10</div><div>PASS: Test 11</div><div>PASS: Test 12</div><div>PASS: Test 13</div></div> + + <script> + function evaluateCode() { + const code = document.getElementById('codeInput').value; + const consoleDiv = document.getElementById('console'); + consoleDiv.innerHTML = ''; + + // Custom console.log function to output to the console div + const originalConsoleLog = console.log; + console.log = function(...args) { + args.forEach(arg => { + const output = document.createElement('div'); + output.textContent = typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg; + consoleDiv.appendChild(output); + }); + originalConsoleLog.apply(console, args); + }; + + try { + eval(code); + } catch (error) { + const errorOutput = document.createElement('div'); + errorOutput.textContent = error; + errorOutput.style.color = 'red'; + consoleDiv.appendChild(errorOutput); + } + + // Restore browser's console.log + console.log = originalConsoleLog; + } + + function downloadCodeAndEditor() { + const codeInput = document.getElementById('codeInput').value; + const htmlContent = document.documentElement.outerHTML.replace( + /<textarea id="codeInput"[^>]*>.*<\/textarea>/, + `<textarea id="codeInput">${codeInput}</textarea>` + ); + + const blob = new Blob([htmlContent], { type: 'text/html' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'code_editor.html'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } + + function shareCode() { + const code = document.getElementById('codeInput').value; + const encodedCode = btoa(encodeURIComponent(code)); + window.location.hash = encodedCode; + window.prompt("Copy the URL to share.\nBe warned! Very long URLs don't share wicked well, sometimes.", window.location.href); + } + + function clearEverything() { + if (!confirm('Are you sure you want to reset the playground?')) { + return; + } else { + window.location.hash = ''; + window.location.reload(); + } + } + + function loadCodeFromHash() { + const hash = window.location.hash.substring(1); + if (hash) { + try { + const decodedCode = decodeURIComponent(atob(hash)); + document.getElementById('codeInput').value = decodedCode; + } catch (error) { + console.error('Failed to decode the URL hash:', error); + } + } + } + + function help() { + const helpText = ` + Welcome to the JavaScript Playground! Here are some tips to get you started: + + 1. Enter your JavaScript code in the textarea. + 2. Click the "Run Code" button to execute your code. + 3. The console output will be displayed below the textarea. + 4. Click the "Clear" button to reset the playground. + 5. Click the "Download" button to save your code and editor as an HTML file. + 6. Click the "Share" button to generate a URL to share your code with others. + 7. You can also press "Cmd + Enter" to run your code. + 8. There's an empty div above the buttons with the id "playground" + 9. You can mount stuff to it using the "mount" function, for more info run "mountHelp()" + 10. You can use the "clear()" function to clear the content's of the console. + + Go nuts! Share scrappy fiddles! + `; + console.log(helpText); + } + + function clear() { + document.getElementById('console').innerHTML = ''; + } + + function mountHelp() { + console.log(` + The mount function is used to mount stuff to the playground div. + It takes a function as an argument, which in turn receives the playground div as an argument. + Before mounting, it clears the playground div. + Here's an example of how to use the mount function: + + mount(playground => { + const h1 = document.createElement('h1'); + h1.textContent = 'Hell is empty and all the devils are here.'; + playground.appendChild(h1); + }); + + This will add an h1 element to the playground div. + `); + } + + function mount(mountFunction) { + const playground = document.getElementById('playground'); + if (!playground) { + console.error("Couldn't find a div with the id 'playground'! You may need to reload the page."); + return; + } + + if (playground.innerHTML.trim() !== "") { + playground.innerHTML = ""; + } + mountFunction(playground); + } + + + document.getElementById('codeInput').addEventListener('keydown', function(event) { + if (event.metaKey && event.key === 'Enter') { + event.preventDefault(); + evaluateCode(); + } + }); + + window.onload = loadCodeFromHash; + </script> + + +</body></html> \ No newline at end of file diff --git a/html/playground/scheme.html b/html/playground/scheme.html new file mode 100644 index 0000000..b8ecd6f --- /dev/null +++ b/html/playground/scheme.html @@ -0,0 +1,533 @@ +<html lang="en"><head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>JavaScript Playground</title> + <meta name="description" content="A JavaScript jungle-gym for doing experiments and sharing scrappy fiddles."> + <style> + body { + display: flex; + flex-direction: column; + align-items: center; + background-color: #ddd; + padding: 10px; + height: 100vh; + margin: 0; + } + + textarea { + width: 100%; + height: 64%; + font-family: monospace; + background-color: #FFFEEC; + border: 2px solid #000; + scrollbar-width: none; + font-size: 1rem; + margin-bottom: 10px; + padding: 10px; + box-sizing: border-box; + resize: none; + border-bottom: 12px solid teal; + -webkit-user-modify: read-write-plaintext-only; + } + + textarea::-webkit-scrollbar { + display: none; + } + + textarea::selection { + background-color: #EFECA7; + } + + textarea:focus { + outline: none; + } + + #console { + width: 100%; + height: 22%; + background-color: #000; + color: #0fc; + font-family: monospace; + font-size: 1rem; + overflow-y: auto; + padding: 10px; + box-sizing: border-box; + white-space: pre-wrap; + } + + .button-container { + width: 100%; + display: flex; + justify-content: flex-end; + margin-bottom: 10px; + } + + button { + padding: 10px 20px; + font-size: 1rem; + margin-left: 10px; + cursor: pointer; + border: none; + transition: background-color 0.3s ease; + } + + button:hover, button:focus { + outline: none; + } + + button.run { + background-color: teal; + color: #FFFFFF; + } + </style> +</head> +<body> + + <div class="playground" id="playground"> + <div class="seesaw" id="seesaw"></div> + <div class="slide" id="slide"></div> + </div> + + <div class="button-container"> + <button onclick="clearEverything()">Clear</button> + <button onclick="downloadCodeAndEditor()">Download</button> + <button onclick="shareCode()">Share</button> + <button onclick="evaluateCode()" class="run">Run Code</button> + </div> + <textarea id="codeInput">function tokenizeScheme(input) { + const tokens = []; + let current = 0; + + const isWhitespace = (char) => /\s/.test(char); + const isDigit = (char) => /[0-9]/.test(char); + const isParen = (char) => char === '(' || char === ')'; + // Symbols can include letters, numbers, and some punctuation like - _ ! ? + const isSymbolChar = (char) => /[a-zA-Z0-9\+\-\*\/\=\?\!\_]/.test(char); + + while (current < input.length) { + let char = input[current]; + + if (isWhitespace(char)) { + current++; + continue; + } + + if (isParen(char)) { + tokens.push({ type: 'paren', value: char }); + current++; + continue; + } + + if (isDigit(char) || (char === '-' && isDigit(input[current + 1]))) { + let number = ''; + while (isDigit(char) || char === '-') { + number += char; + char = input[++current]; + } + tokens.push({ type: 'number', value: number }); + continue; + } + + // Handle symbols, including letters, numbers, punctuation + if (isSymbolChar(char)) { + let symbol = ''; + while (isSymbolChar(char)) { + symbol += char; + char = input[++current]; + } + tokens.push({ type: 'symbol', value: symbol }); + continue; + } + + throw new Error(`Unexpected character: ${char}`); + } + + return tokens; +} + + +function parseScheme(tokens) { + let current = 0; + + function walk() { + let token = tokens[current]; + + if (token.type === 'number') { + current++; + return { type: 'NumberLiteral', value: Number(token.value) }; + } + + if (token.type === 'symbol') { + current++; + return { type: 'Symbol', value: token.value }; + } + + if (token.type === 'paren' && token.value === '(') { + current++; + const node = { type: 'List', value: [] }; + + while (!(tokens[current].type === 'paren' && tokens[current].value === ')')) { + node.value.push(walk()); + } + + current++; // Skip closing ')' + return node; + } + + throw new Error(`Unexpected token: ${token.type}`); + } + + return walk(); +} + +const globalEnv = { + '+': (...args) => args.reduce((acc, curr) => acc + curr), + '-': (...args) => args.reduce((acc, curr) => acc - curr), + '*': (...args) => args.reduce((acc, curr) => acc * curr), + '/': (a, b) => a / b, // Only two arguments for division + 'eq?': (...args) => args.every((val, i, arr) => val === arr[0]), + 'car': (list) => { + if (list.type !== 'List' || list.value.length === 0) { + throw new Error('car expects a non-empty list'); + } + return list.value[0]; + }, + 'cdr': (list) => { + if (list.type !== 'List' || list.value.length === 0) { + throw new Error('cdr expects a non-empty list'); + } + return { type: 'List', value: list.value.slice(1) }; + }, + 'cons': (a, b) => { + if (b.type !== 'List') { + throw new Error('cons expects second argument to be a list'); + } + return { type: 'List', value: [a].concat(b.value) }; + }, + 'null?': (list) => list.type === 'List' && list.value.length === 0, + 'zero?': (n) => n === 0, + 'atom?': (x) => typeof x !== 'object' || x === null, + 'number?': (x) => typeof x === 'number', + 'add1': (n) => n + 1, + 'sub1': (n) => n - 1, + 'quote': (x) => x, // Simply return the quoted expression + 'and': (...args) => args.every(Boolean), + 'or': (...args) => args.some(Boolean), + 'true': true, + 'false': false +}; + + + + +function evaluate(node, env = globalEnv) { + if (node.type === 'NumberLiteral') { + return node.value; + } + + if (node.type === 'Symbol') { + if (env[node.value] !== undefined) { + return env[node.value]; + } + throw new Error(`Undefined symbol: ${node.value}`); + } + + if (node.type === 'List') { + const [first, ...rest] = node.value; + + // Is the first element a symbol, like an operator or function name? + if (first.type === 'Symbol') { + const operator = first.value; + + // Special case for define + if (operator === 'define') { + const [symbol, expr] = rest; + env[symbol.value] = evaluate(expr, env); + return; + } + + // Special case for lambda + if (operator === 'lambda') { + const [params, body] = rest; + + // Create a closure to return + return function (...args) { + const lambdaEnv = { ...env }; + + // Bind each argument to the corresponding parameter... + params.value.forEach((param, i) => { + lambdaEnv[param.value] = args[i]; + }); + + // ...and then evaluate the body with the environment + return evaluate(body, lambdaEnv); + }; + } + + // Special case for if + if (operator === 'if') { + const [test, consequent, alternate] = rest; + const condition = evaluate(test, env); + return condition ? evaluate(consequent, env) : evaluate(alternate, env); + } + + // Special case for quote + if (operator === 'quote') { + return rest[0]; // Return the quoted expression without evaluating it + } + + // Special case for cond + if (operator === 'cond') { + for (let clause of rest) { + const [test, expr] = clause.value; + if (evaluate(test, env)) { + return evaluate(expr, env); + } + } + return null; // No matching condition + } + + // Special case for letrec (recursive let) + if (operator === 'letrec') { + const [bindings, body] = rest; + const letEnv = { ...env }; + + // Loop through bindings and evaluate each + bindings.value.forEach(binding => { + const [name, expr] = binding.value; + letEnv[name.value] = evaluate(expr, letEnv); + }); + + return evaluate(body, letEnv); + } + } + + // Evaluate the first element + const func = evaluate(first, env); + + if (typeof func !== 'function') { + throw new Error(`Expected a function but got: ${func}`); + } + + const args = rest.map(arg => evaluate(arg, env)); + return func(...args); + } + + throw new Error(`Unexpected node type: ${node.type}`); +} + + + +function evalScheme(input) { + const tokens = tokenizeScheme(input); + const ast = parseScheme(tokens); + return evaluate(ast); +} + + + + + + + +function mountRepl(playground) { + // Create a REPL container + const replContainer = document.createElement('div'); + replContainer.style.display = 'flex'; + replContainer.style.flexDirection = 'column'; + replContainer.style.width = '100%'; + + // Create an input field for the Scheme expressions + const input = document.createElement('textarea'); + input.placeholder = "Scheme here..."; + input.style.width = '100%'; + input.style.height = '100px'; + input.style.marginBottom = '10px'; + input.style.fontFamily = 'monospace'; + + // Create a button to evaluate the expression + const evalButton = document.createElement('button'); + evalButton.textContent = 'Evaluate'; + + // Create a container to display the results + const output = document.createElement('pre'); + output.style.width = '100%'; + output.style.height = '200px'; + output.style.overflowY = 'auto'; + output.style.backgroundColor = '#f0f0f0'; + output.style.padding = '10px'; + output.style.fontFamily = 'monospace'; + + // Add the input, button, and output to the REPL container + replContainer.appendChild(input); + replContainer.appendChild(evalButton); + replContainer.appendChild(output); + + // Add the REPL container to the playground div + playground.appendChild(replContainer); + + evalButton.addEventListener('click', () => { + const expression = input.value.trim(); + if (expression) { + try { + // Evaluate the expression + const result = evalScheme(expression); + // Append the result to the output area + output.textContent += `> ${expression}\n${result}\n\n`; + } catch (error) { + // Error if the expression is invalid + output.textContent += `> ${expression}\nError: ${error.message}\n\n`; + } + } + // Clear input after evaluation + input.value = ''; + }); +} + + +mount(mountRepl);</textarea> + <div id="console"></div> + + <script> + function evaluateCode() { + const code = document.getElementById('codeInput').value; + const consoleDiv = document.getElementById('console'); + consoleDiv.innerHTML = ''; + + // Custom console.log function to output to the console div + const originalConsoleLog = console.log; + console.log = function(...args) { + args.forEach(arg => { + const output = document.createElement('div'); + output.textContent = typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg; + consoleDiv.appendChild(output); + }); + originalConsoleLog.apply(console, args); + }; + + try { + eval(code); + } catch (error) { + const errorOutput = document.createElement('div'); + errorOutput.textContent = error; + errorOutput.style.color = 'red'; + consoleDiv.appendChild(errorOutput); + } + + // Restore browser's console.log + console.log = originalConsoleLog; + } + + function downloadCodeAndEditor() { + const codeInput = document.getElementById('codeInput').value; + const htmlContent = document.documentElement.outerHTML.replace( + /<textarea id="codeInput"[^>]*>.*<\/textarea>/, + `<textarea id="codeInput">${codeInput}</textarea>` + ); + + const blob = new Blob([htmlContent], { type: 'text/html' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'code_editor.html'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } + + function shareCode() { + const code = document.getElementById('codeInput').value; + const encodedCode = btoa(encodeURIComponent(code)); + window.location.hash = encodedCode; + window.prompt("Copy the URL to share.\nBe warned! Very long URLs don't share wicked well, sometimes.", window.location.href); + } + + function clearEverything() { + if (!confirm('Are you sure you want to reset the playground?')) { + return; + } else { + window.location.hash = ''; + window.location.reload(); + } + } + + function loadCodeFromHash() { + const hash = window.location.hash.substring(1); + if (hash) { + try { + const decodedCode = decodeURIComponent(atob(hash)); + document.getElementById('codeInput').value = decodedCode; + } catch (error) { + console.error('Failed to decode the URL hash:', error); + } + } + } + + function help() { + const helpText = ` + Welcome to the JavaScript Playground! Here are some tips to get you started: + + 1. Enter your JavaScript code in the textarea. + 2. Click the "Run Code" button to execute your code. + 3. The console output will be displayed below the textarea. + 4. Click the "Clear" button to reset the playground. + 5. Click the "Download" button to save your code and editor as an HTML file. + 6. Click the "Share" button to generate a URL to share your code with others. + 7. You can also press "Cmd + Enter" to run your code. + 8. There's an empty div above the buttons with the id "playground" + 9. You can mount stuff to it using the "mount" function, for more info run "mountHelp()" + 10. You can use the "clear()" function to clear the content's of the console. + + Go nuts! Share scrappy fiddles! + `; + console.log(helpText); + } + + function clear() { + document.getElementById('console').innerHTML = ''; + } + + function mountHelp() { + console.log(` + The mount function is used to mount stuff to the playground div. + It takes a function as an argument, which in turn receives the playground div as an argument. + Before mounting, it clears the playground div. + Here's an example of how to use the mount function: + + mount(playground => { + const h1 = document.createElement('h1'); + h1.textContent = 'Hell is empty and all the devils are here.'; + playground.appendChild(h1); + }); + + This will add an h1 element to the playground div. + `); + } + + function mount(mountFunction) { + const playground = document.getElementById('playground'); + if (!playground) { + console.error("Couldn't find a div with the id 'playground'! You may need to reload the page."); + return; + } + + if (playground.innerHTML.trim() !== "") { + playground.innerHTML = ""; + } + mountFunction(playground); + } + + + document.getElementById('codeInput').addEventListener('keydown', function(event) { + if (event.metaKey && event.key === 'Enter') { + event.preventDefault(); + evaluateCode(); + } + }); + + window.onload = loadCodeFromHash; + </script> + + +</body></html> \ No newline at end of file diff --git a/html/schemer/index.html b/html/schemer/index.html new file mode 100644 index 0000000..2220e57 --- /dev/null +++ b/html/schemer/index.html @@ -0,0 +1,305 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Scheme Interpreter with PDF</title> + <style> + body, html { + margin: 0; + padding: 0; + height: 100%; + font-family: Arial, sans-serif; + } + #pdf-container { + width: 100%; + height: 67%; + overflow: hidden; + } + #repl-container { + width: 100%; + height: 33%; + display: flex; + flex-direction: column; + border-top: 1px solid #ccc; + padding: 10px; + } + textarea { + flex: 1; + width: 100%; + font-family: monospace; + font-size: 16px; + padding: 10px; + } + button { + margin-top: 10px; + padding: 10px; + font-size: 16px; + } + #scheme-output { + font-family: monospace; + background-color: #f0f0f0; + padding: 10px; + margin-top: 10px; + overflow-wrap: break-word; + } + </style> +</head> +<body> + + <div id="pdf-container"> + <embed src="tls.pdf" type="application/pdf" width="100%" height="100%"> + </div> + + <div id="repl-container"> + <textarea id="scheme-input" placeholder="Scheme here..."></textarea> + <div id="scheme-output"></div> + <button onclick="evaluateScheme()">Run Code</button> + </div> + + <script> + function tokenizeScheme(input) { + const tokens = []; + let current = 0; + + const isWhitespace = (char) => /\s/.test(char); + const isDigit = (char) => /[0-9]/.test(char); + const isParen = (char) => char === '(' || char === ')'; + // Symbols can include letters, numbers, and some punctuation like - _ ! ? + const isSymbolChar = (char) => /[a-zA-Z0-9\+\-\*\/\=\?\!\_]/.test(char); + + while (current < input.length) { + let char = input[current]; + + if (isWhitespace(char)) { + current++; + continue; + } + + if (isParen(char)) { + tokens.push({ type: 'paren', value: char }); + current++; + continue; + } + + if (isDigit(char) || (char === '-' && isDigit(input[current + 1]))) { + let number = ''; + while (isDigit(char) || char === '-') { + number += char; + char = input[++current]; + } + tokens.push({ type: 'number', value: number }); + continue; + } + + // Handle symbols, including letters, numbers, punctuation + if (isSymbolChar(char)) { + let symbol = ''; + while (isSymbolChar(char)) { + symbol += char; + char = input[++current]; + } + tokens.push({ type: 'symbol', value: symbol }); + continue; + } + + throw new Error(`Unexpected character: ${char}`); + } + + return tokens; + } + + + function parseScheme(tokens) { + let current = 0; + + function walk() { + let token = tokens[current]; + + if (token.type === 'number') { + current++; + return { type: 'NumberLiteral', value: Number(token.value) }; + } + + if (token.type === 'symbol') { + current++; + return { type: 'Symbol', value: token.value }; + } + + if (token.type === 'paren' && token.value === '(') { + current++; + const node = { type: 'List', value: [] }; + + while (!(tokens[current].type === 'paren' && tokens[current].value === ')')) { + node.value.push(walk()); + } + + current++; // Skip closing ')' + return node; + } + + throw new Error(`Unexpected token: ${token.type}`); + } + + return walk(); + } + + const globalEnv = { + '+': (...args) => args.reduce((acc, curr) => acc + curr), + '-': (...args) => args.reduce((acc, curr) => acc - curr), + '*': (...args) => args.reduce((acc, curr) => acc * curr), + '/': (a, b) => a / b, // Only two arguments for division + 'eq?': (...args) => args.every((val, i, arr) => val === arr[0]), + 'car': (list) => { + if (list.type !== 'List' || list.value.length === 0) { + throw new Error('car expects a non-empty list'); + } + return list.value[0]; + }, + 'cdr': (list) => { + if (list.type !== 'List' || list.value.length === 0) { + throw new Error('cdr expects a non-empty list'); + } + return { type: 'List', value: list.value.slice(1) }; + }, + 'cons': (a, b) => { + if (b.type !== 'List') { + throw new Error('cons expects second argument to be a list'); + } + return { type: 'List', value: [a].concat(b.value) }; + }, + 'null?': (list) => list.type === 'List' && list.value.length === 0, + 'zero?': (n) => n === 0, + 'atom?': (x) => typeof x !== 'object' || x === null, + 'number?': (x) => typeof x === 'number', + 'add1': (n) => n + 1, + 'sub1': (n) => n - 1, + 'quote': (x) => x, // Simply return the quoted expression + 'and': (...args) => args.every(Boolean), + 'or': (...args) => args.some(Boolean), + 'true': true, + 'false': false + }; + + + + + function evaluate(node, env = globalEnv) { + if (node.type === 'NumberLiteral') { + return node.value; + } + + if (node.type === 'Symbol') { + if (env[node.value] !== undefined) { + return env[node.value]; + } + throw new Error(`Undefined symbol: ${node.value}`); + } + + if (node.type === 'List') { + const [first, ...rest] = node.value; + + // Is the first element a symbol, like an operator or function name? + if (first.type === 'Symbol') { + const operator = first.value; + + // Special case for define + if (operator === 'define') { + const [symbol, expr] = rest; + env[symbol.value] = evaluate(expr, env); + return; + } + + // Special case for lambda + if (operator === 'lambda') { + const [params, body] = rest; + + // Create a closure to return + return function (...args) { + const lambdaEnv = { ...env }; + + // Bind each argument to the corresponding parameter... + params.value.forEach((param, i) => { + lambdaEnv[param.value] = args[i]; + }); + + // ...and then evaluate the body with the environment + return evaluate(body, lambdaEnv); + }; + } + + // Special case for if + if (operator === 'if') { + const [test, consequent, alternate] = rest; + const condition = evaluate(test, env); + return condition ? evaluate(consequent, env) : evaluate(alternate, env); + } + + // Special case for quote + if (operator === 'quote') { + return rest[0]; // Return the quoted expression without evaluating it + } + + // Special case for cond + if (operator === 'cond') { + for (let clause of rest) { + const [test, expr] = clause.value; + if (evaluate(test, env)) { + return evaluate(expr, env); + } + } + return null; // No matching condition + } + + // Special case for letrec (recursive let) + if (operator === 'letrec') { + const [bindings, body] = rest; + const letEnv = { ...env }; + + // Loop through bindings and evaluate each + bindings.value.forEach(binding => { + const [name, expr] = binding.value; + letEnv[name.value] = evaluate(expr, letEnv); + }); + + return evaluate(body, letEnv); + } + } + + // Evaluate the first element + const func = evaluate(first, env); + + if (typeof func !== 'function') { + throw new Error(`Expected a function but got: ${func}`); + } + + const args = rest.map(arg => evaluate(arg, env)); + return func(...args); + } + + throw new Error(`Unexpected node type: ${node.type}`); + } + + + + function evalScheme(input) { + const tokens = tokenizeScheme(input); + const ast = parseScheme(tokens); + return evaluate(ast); + } + + // Function to evaluate the input in the REPL + function evaluateScheme() { + const input = document.getElementById('scheme-input').value; + let output; + try { + output = evalScheme(input); + } catch (error) { + output = `Error: ${error.message}`; + } + document.getElementById('scheme-output').innerText = JSON.stringify(output, null, 2); + } + </script> + +</body> +</html> diff --git a/html/schemer/tls.pdf b/html/schemer/tls.pdf new file mode 100644 index 0000000..7e28a5f --- /dev/null +++ b/html/schemer/tls.pdf Binary files differdiff --git a/html/squine.html b/html/squine.html new file mode 100644 index 0000000..cd2c0e1 --- /dev/null +++ b/html/squine.html @@ -0,0 +1,1108 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <!-- +MIT License + +Copyright (c) m15o + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +--> + <meta charset="utf-8"> + <title>squine</title> +</head> +<style> + html, body, #app, .column, textarea, table, td { + height: 100%; + } + + body { + padding: 0; + margin: 0; + background: linear-gradient(to bottom, #e4d16f, #d5363d); + } + + header { + display: flex; + border-bottom: 1px solid; + border-top: 1px solid; + } + + header > * { + border-right: 1px solid; + } + + .column { + display: flex; + flex-direction: column; + min-width: 10px; + } + + article, section { + display: flex; + flex-direction: column; + overflow: hidden; + } + + .name { + box-sizing: border-box; + width: 100%; + padding: 2px 5px; + margin: 0; + outline: none; + border: 0; + border-right: 1px solid; + background-color: #fffad6; + } + + #search { + display: none; + } + + #search.searching { + display: block; + position: fixed; + top: 0; + left: 0; + right: 0; + background-color: darkblue; + color: white; + } + + textarea { + box-sizing: border-box; + padding: 5px; + margin: 0; + border: 0; + outline: none; + resize: none; + background-color: #fff9e9; + } + + .action { + padding: 2px 5px; + cursor: pointer; + } + + .back, .forward { + background-color: silver; + } + + .hist { + background-color: inherit; + } + + .folded article, .folded .h-resize, .maxed .h-resize { + display: none; + } + + .maxed article { + display: flex; + } + + .folded .maxed, .maxed article { + flex-grow: 1; + } + + section:last-child, section:last-child article { + flex-grow: 1; + } + + .folded section { + flex-grow: 0; + } + + table { + width: 100%; + border-collapse: collapse; + } + + td { + padding: 0; + border: 1px solid; + } + + .menu > * { + padding: 0 4px; + cursor: pointer; + } + + .menu > *:hover { + background-color: #dfad6a; + } + + .active-p .content { + background-color: white; + } + + .active { + background-color: #e5d575; + } + + .col { + position: relative; + } + + .h-resize:hover, .resize:hover { + background-color: rebeccapurple; + } + + .resize { + width: 2px; + background-color: silver; + cursor: col-resize; + position: absolute; + top: 0; + right: 0; + bottom: 0; + } + + #settings { + display: none; + } + + #settings.visible { + display: table-row; + } + + .dirty .save { + background-color: aquamarine; + } + + .h-resize { + height: 2px; + background-color: darkgrey; + cursor: row-resize; + } + + .parse { + background-color: #dfad6a; + } +</style> +<body> +<div id="app"></div> +<script> + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Helpers + + function $(elt) { + return document.querySelector(elt); + } + + function $$(elt) { + return document.querySelectorAll(elt); + } + + function $$active(elt) { + return $panel => ($panel && $panel.querySelector(elt)) || $('.active-p ' + elt); + } + + function $name($panel) { + return $$active('.name')($panel); + } + + function $content($panel) { + return $$active('.content')($panel); + } + + function t$(e) { + return e.target; + } + + function closest$(e, sel) { + return t$(e).closest(sel); + } + + function $search() { + return $('#search'); + } + + function $panel() { + return $('.active-p'); + } + + function hasCls($elt, cls) { + return $elt.classList.contains(cls); + } + + function date() { + return (new Date()).toLocaleDateString('en-CA'); + } + + function makeElt(elt, cls, attrs, html, value) { + let e = document.createElement(elt); + cls.forEach(c => e.classList.add(c)); + Object.keys(attrs).forEach(k => e.setAttribute(k, attrs[k])); + html && (e.innerHTML = html); + value && (e.value = value); + return e; + } + + function togCls(cls, sel, $elt) { + $$(sel).forEach((e) => e.classList.remove(cls)); + $elt.classList.add(cls); + } + + function setActive($section) { + togCls('active-p', 'section', $section); + } + + function closeSearch() { + $search().classList.remove('searching'); + $content().focus(); + } + + function refPane(name) { + let p; + $$('section').forEach(e => e.querySelector('.name').value.startsWith('+ref') && (p = e)); + if (!p) { + p = createPane(); + } + setActive(p); + load(name); + return p; + } + + function runPane(content) { + let p; + save(); + let old = $panel(); + $$('section').forEach(e => e.querySelector('.name').value.startsWith('+run') && (p = e)); + let err, rv; + storeKeys().forEach(k => { + if (k === 'main') return; + try { + global[k] = eval(global, parse(storeGet(k)), true); + } catch (e) { + err = `in ${k}: ${e}` + } + }); + if (!p) { + p = createPane(); + load('+run'); + } + setActive(p); + if (err) { + $content().value = err; + return; + } + try { + $content().value = asString(exec(storeGet('main'))); + } catch (e) { + $content().value = e; + } + setActive(old); + $content().focus(); + } + + + // function runPane(content) { + // let p; + // $$('section').forEach(e => e.querySelector('.name').value.startsWith('+run') && (p = e)); + // let err, rv; + // try { + // if ($name().value !== 'main') { + // global[$name().value] = eval(global, parse(content), true); + // rv = asString(global[$name().value]); + // } + // } catch (e) { + // err = e; + // } + // + // if (!p) { + // p = createPane(); + // load('+run'); + // } + // setActive(p); + // if (err) { + // $content().value = err; + // return; + // } + // if (rv) { + // $content().value = rv; + // return; + // } + // try { + // $content().value = asString(exec(content)); + // } catch (e) { + // $content().value = e; + // } + // } + + function isChordNav(e) { + return e.metaKey || e.ctrlKey; + } + + function isChordNavBlank(e) { + return isChordNav(e) && e.altKey; + } + + function navBlank(name) { + findOrCreatePane(name); + } + + function debounce(fn, ms) { + let timeout; + return (...args) => { + clearTimeout(timeout); + timeout = setTimeout(() => fn(...args), ms); + } + } + + function basename(path) { + const n = path.split('/').pop(); + return n.split('.').shift(); + } + + function download(data, name, mime) { + const link = document.createElement('a'); + link.download = name; + link.href = window.URL.createObjectURL(new Blob([data], {type: mime})); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // storage + + let store = {}; + + function storeSet(k, v) { + if (isStoreKey(k) && v === storeGet(k, v)) return; + $('#app').classList.add('dirty'); + store[k] = v; + } + + function storeGet(k) { + return store[k] || ''; + } + + function storeDel(k) { + delete store[k]; + } + + function storeKeys(sorted) { + let rv = Object.keys(store); + if (sorted) return rv.sort(); + return rv; + } + + function isStoreKey(k) { + return storeKeys().includes(k); + } + + function storeClear() { + store = {}; + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Core + + function save() { + let name = $name().value; + let content = $content().value; + $panel().dataset['name'] = name; + if (!name || name === '+run') return; + storeSet(name, content); + refresh(); + } + + const saveD = debounce(save, 200); + + function updateHistory(name) { + let cname = $panel().dataset['name']; + if (!name || name === cname) return; + hpush(hist().back, cname); + hist().forward = []; + } + + function ls(prefix) { + return storeKeys(true).filter(k => !prefix || k.startsWith(prefix)).join('\n'); + } + + function orph() { + return storeKeys().map(k => { + const v = storeGet(k); + const m = [...v.matchAll(/\[\[([\w\.\-]+)\]\]/g)].map(match => match[1]); + const r = m.filter(e => !storeGet(e)); + if (!r.length) return null; + return ['[[' + k + ']]', '----------', ...r.map(e => '[[' + e + ']]'), ''].join('\n'); + }).filter(Boolean).join('\n'); + } + + function load(name, noHist) { + if (!name) { + $name().value = ''; + $content().value = ''; + return; + } + !noHist && updateHistory(name) + if (name === '+orph') { + $content().value = orph(); + } else if (name.startsWith('+ls')) { + $content().value = ls(name.split(':')[1]); + } else if (name.startsWith('+search')) { + $content().value = name.split(':')[1] ? lookup(name.split(':')[1]) : ''; + } else if (name.startsWith('+ref')) { + $content().value = name.split(':')[1] ? lookup('[[' + name.split(':')[1] + ']]') : ''; + } else { + $content().value = storeGet(name); + } + $name().value = name; + $panel().dataset['name'] = name; + $panel().querySelector('.back').classList.toggle('hist', !!hist().back.length); + $panel().querySelector('.forward').classList.toggle('hist', !!hist().forward.length); + } + + function refresh() { + let $elt = $('#files'); + $elt.innerHTML = ''; + storeKeys().forEach(k => $elt.appendChild(makeElt('option', [], {}, null, k))); + $elt = $('#globals'); + $elt.innerHTML = ''; + Object.keys(global).forEach(k => $elt.appendChild(makeElt('option', [], {}, null, k))); + } + + let paneID = 0; + + function createHistory(paneID) { + history[paneID] = {back: [], forward: []}; + } + + function removeHistory(paneID) { + delete history[paneID]; + } + + function deletePane($pane) { + removeHistory($pane.dataset['id']); + $pane.remove(); + } + + function createPane() { + const header = document.createElement('header'); + header.append( + makeElt('span', ['back', 'action'], {}, '<'), + makeElt('span', ['forward', 'action'], {}, '>'), + makeElt('input', ['name'], {list: 'files', autocomplete: 'off'}), + makeElt('span', ['max', 'action'], {}, '+'), + makeElt('span', ['move', 'action'], {}, '~'), + makeElt('span', ['close', 'action'], {}, 'x') + ); + const id = paneID++; + const section = document.createElement('section'); + const article = document.createElement('article'); + article.append(makeElt('textarea', ['content'], {spellcheck: false, onkeyup: 'saveD()'})); + section.setAttribute('data-id', '' + id); + section.append( + header, + makeElt('div', ['parse'], {}), + article, + makeElt('div', ['h-resize'], {}) + ); + createHistory(id); + $(".column.active").appendChild(section); + setActive(section); + $content().focus(); + return section; + } + + function lookup(str) { + return storeKeys(true).map(k => { + const v = storeGet(k); + const m = v.split('\n').filter(l => l.includes(str)); + if (!m.length) return null; + return ['[[' + k + ']]', '----------', ...m, ''].join('\n'); + }).filter(Boolean).join('\n'); + } + + function mv(before, after) { + return storeKeys().forEach(k => { + const v = storeGet(k); + storeSet(k, v.replaceAll('[[' + before + ']]', '[[' + after + ']]')) + }) + } + + function link(textarea) { + const text = textarea.value; + const pos = textarea.selectionStart; + let start, end; + + for (start = pos - 1; start > -1 && /[^\s\(\)]/.test(text[start]); start--) { + } + + for (end = pos; end < text.length && /[^\s\(\)]/.test(text[end]); end++) { + } + + return text.substring(start + 1, end); + } + + function insert(textarea, text) { + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const before = textarea.value.substring(0, start); + const after = textarea.value.substring(end, textarea.value.length); + + textarea.value = before + text + after; + textarea.selectionStart = textarea.selectionEnd = start + text.length; + } + + const write = text => document.execCommand('insertText', false, text); + + const history = {}; + + function hist() { + return history[$panel().dataset['id']]; + } + + function hpush(h, name) { + if (h[h.length - 1] === name) return; + h.push(name); + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Menu + + function menuNew() { + createPane(); + } + + function menuLs() { + $name() || createPane(); + load('+ls'); + } + + function menuReset() { + if (confirm('delete everything?')) { + storeClear(); + } + } + + function menuRun() { + runPane($content().value); + } + + function menuMv() { + let prev = $panel().dataset['name']; + if (prev === $name().value) return; + mv(prev, $name().value); + save(); + storeDel(prev); + $prevPanel = $panel(); + $$('section').forEach($pane => { + setActive($pane); + load($name().value, false); + }) + setActive($prevPanel); + refresh(); + } + + function quine() { + const regex = /let store = (.*)/; + return ['<!DOCTYPE html>', + `<head>${document.head.innerHTML}</head>`, + '<body>', + '<div id="app"></div>', + `<script>${$('script').innerHTML.replace(regex, "let store = " + JSON.stringify(store) + .replaceAll('</' + 'script', "' + '</' + 'script' + '") + ';') + '</'}script>`, + '</body>'].join('\n'); + } + + function menuSave() { + $('#app').classList.remove('dirty'); + download(quine(), basename(window.location.href) + '.html', 'text/html'); + } + + function menuExport() { + download(JSON.stringify(store), basename(window.location.href) + '.json', 'text/json'); + } + + function menuDel() { + let name = $name().value; + if (name && confirm('delete ' + name + '?')) { + storeDel(name); + refresh(); + deletePane($panel()); + } + } + + function menuSettings() { + $('#settings').classList.toggle('visible'); + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Pane actions + + function paneFold(e) { + closest$(e, 'section').classList.toggle('folded'); + } + + function paneMax(e) { + if (closest$(e, 'section').classList.contains('maxed')) { + closest$(e, 'section').classList.remove('maxed'); + closest$(e, '.column').classList.remove('folded'); + } else { + closest$(e, '.column').querySelectorAll('section').forEach(e => e.classList.remove("maxed")); + closest$(e, 'section').classList.add("maxed"); + closest$(e, '.column').classList.add('folded'); + } + } + + function paneMove(e) { + let $c; + $$('.column').forEach(c => { + if (c !== closest$(e, '.column')) $c = c; + }); + $c.appendChild(closest$(e, 'section')); + } + + function paneClose(e) { + deletePane(closest$(e, 'section')); + if ($('section')) { + setActive($('section')); + $content().focus; + } + } + + function paneHist(from, to) { + return function (e) { + if (!from.length) return; + let name = from.pop(); + if (isChordNavBlank(e)) { + from.push(name); + createPane(); + } else if (isChordNav(e)) { + from.push(name); + setActive($prevPanel); + load(name); + $content().focus(); + } else { + hpush(to, $panel().dataset['name']); + } + load(name, true); + } + } + + function findOrCreatePane(name) { + let p; + $$('section').forEach(e => e.querySelector('.name').value === name && (p = e)); + if (!p) { + p = createPane(); + } + setActive(p); + load(name); + return p; + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Scheme interpreter + + const global = {}; + const arity = (fn, n) => (s, env) => { + if (collect(cdr(s)).length !== n) throw new Error(`${asString(car(s))}: Wrong number of arguments`); + return fn(s, env); + } + const primitive = { + define: arity((s, env) => { + return env[cadr(s)] = eval(env, cadr(cdr(s))); + }, 2), + quote: arity((s, env) => cadr(s), 1), + car: arity((s, env) => { + let rv = eval(env, cadr(s)); + if (rv === null) throw new Error('You cannot ask for the car of the empty list'); + else if (isAtom(rv)) throw new Error('You cannot ask for the car of an atom'); + return car(rv) + }, 1), + cdr: arity((s, env) => { + let rv = eval(env, cadr(s)); + if (rv === null) throw new Error('You cannot ask for the cdr of the empty list'); + if (isAtom(rv)) throw new Error('You cannot ask for the cdr of an atom'); + return cdr(rv) + }, 1), + cons: arity((s, env) => { + let r = eval(env, cadr(cdr(s))); + if (!isList(r)) throw new Error('Second argument of cons must be a list'); + return cons(eval(env, cadr(s)), r); + }, 2), + 'null?': arity((s, env) => { + let r = eval(env, cadr(s)); + if (!isList(r)) throw new Error('null? is defined only for lists'); + return r === null; + }, 1), + 'zero?': arity((s, env) => { + let r = eval(env, cadr(s)); + if (!isAtom(r)) throw new Error('zero? is defined only for atoms'); + return r === 0; + }, 1), + 'atom?': arity((s, env) => { + return isAtom(eval(env, cadr(s))); + }, 1), + 'number?': arity((s, env) => { + return isNumber(eval(env, cadr(s))); + }, 1), + 'eq?': arity((s, env) => { + let l = eval(env, cadr(s)); + let r = eval(env, cadr(cdr(s))); + if (!isAtom(l) || isNumber(l) || !isAtom(r) || isNumber(r)) throw new Error('eq? takes two non-numeric atoms'); + return l === r; + }, 2), + or: arity((s, env) => { + return eval(env, cadr(s)) || eval(env, cadr(cdr(s))); + }, 2), + and: arity((s, env) => { + return eval(env, cadr(s)) && eval(env, cadr(cdr(s))); + }, 2), + add1: arity((s, env) => eval(env, cadr(s)) + 1, 1), + sub1: arity((s, env) => eval(env, cadr(s)) - 1, 1), + lambda: arity((s, env) => { + return { + args: collect(cadr(s)), body: cadr(cdr(s)), + env: Object.fromEntries(Object.entries(env)) + } + }, 2), + letrec: arity((s, env) => { + let f = eval(env, car(cdr(car(cadr(s))))); + f.env[car(car(car(cdr(s))))] = f; + return eval(f.env, car(cdr(cdr(s)))); + }, 2), + cond: (s, env) => { + let b = collect(cdr(s)); + for (let i = 0; i < b.length; i++) { + if (car(b[i]) === 'else' || eval(env, car(b[i]))) + return eval(env, cadr(b[i])); + } + } + } + const cons = (car, cdr) => [car, cdr]; + const car = c => c[0]; + const cdr = c => c[1]; + const cadr = c => car(cdr(c)); + const isAtom = s => s !== null && !Array.isArray(s); + const isList = s => Array.isArray(s) || s === null; + const isNumber = s => typeof s === 'number'; + const parse = src => ast(src.replaceAll('(', ' ( ').replaceAll(')', ' ) ').replaceAll(/;.*$/gm, '').split(/\s/).filter(Boolean)); + const exec = src => eval(global, parse(src), true); + + function ast(tokens, d = {depth: 0}) { + if (!tokens.length) { + if (d.depth > 0) throw new Error('Unexpected EOF!'); + else return null; + } + let t = tokens.shift(); + if (t === ')') { + if (d.depth-- === 0) throw new Error('Unexpected closing parenthesis'); + return null; + } else if (t === '(') { + d.depth++; + return cons(ast(tokens, d), ast(tokens, d)); + } else if (t[0] === '#') { + if (tokens.length) return cons(t[1] === 't', ast(tokens, d)); + return t[1] === 't'; + } else if (isNaN(t)) { + if (tokens.length) return cons(t, ast(tokens, d)); + return t; + } + if (tokens.length) return cons(+t, ast(tokens, d)); + return +t; + } + + function collect(s) { + if (!s) return []; + return [car(s), ...collect(cdr(s))]; + } + + function eval(env, s, isSrc) { + if (s === null) throw new Error('Evaluating empty list'); + if (isAtom(s)) { + if (isNaN(s)) { + if (env[s] !== undefined) return env[s]; + else if (Object.keys(primitive).includes(s)) return s; + throw new Error('Undefined variable ' + s); + } + return s; + } else if (isSrc) { + let rv = eval(env, car(s)); + if (cdr(s)) return eval(env, cdr(s), isSrc); + return rv; + } + let proc = eval(env, car(s)); + if (Object.keys(primitive).includes(proc || car(s))) { + return primitive[proc || car(s)](s, env) + } + try { + let args = {}; + collect(cdr(s)).forEach((a, i) => { + args[proc.args[i]] = eval(env, a); + }); + return arity((_, env) => eval(env, proc.body), proc.args.length)(s, Object.assign({}, env, proc.env, args)); + } catch (e) { + throw new Error(`${asString(car(s))}: ${e}`); + } + } + + function asString(s) { + function build(s, acc = [], nl = true) { + if (s === null) { + acc.push('()'); + return acc; + } else if (s.body) { + acc.push('<proc (' + s.args.join(' ') + ')>'); + return acc; + } else if (isAtom(s)) { + acc.push(s); + return acc; + } else { + nl && acc.push('('); + build(s[0], acc, true); + s[1] && build(s[1], acc, false); + nl && acc.push(')'); + } + return acc; + } + + return build(s).join(' ').replaceAll('( ', '(').replaceAll(' )', ')'); + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Listeners + + let $prevPanel; + let $resize; + let $hresize; + + function handleSearch(e) { + e.preventDefault(); + e.stopPropagation(); + if (isChordNavBlank(e)) { + navBlank(t$(e).value); + closeSearch(); + } else if (isChordNav(e)) { + if (!$content()) createPane(); + closeSearch(); + load(t$(e).value); + } else { + if (!$content()) return; + closeSearch(); + if (!isStoreKey(t$(e).value)) { + storeSet(t$(e).value, ''); + refresh(); + } + write(t$(e).value); + } + } + + document.getElementById('app').innerHTML = ` + <datalist id="files"></datalist> + <datalist id="globals"></datalist> +<table> + <tr> + <td colspan="2" class="menu" style="height: 0;"> + <span class="new">new</span><span class="ls">ls</span><span class="mv">mv</span><span class="del">del</span><span class="save">save</span><span class="run">run</span><span class="settings">import</span><span class="export">export</span><span class="reset">reset</span> + </td> + </tr> + <tr id="settings"> + <td colspan="2" class="menu" style="height: 0;"> + <input type="file" id="import" value=""> + </td> + </tr> + <tr> + <td class="col"> + <div class="active column"></div> + <div class="resize"></div> + </td> + <td> + <div class="column"></div> + </td> + </tr> +</table> +<input list="files" spellcheck="false" id="search" autocomplete="off"/> + `; + + document.addEventListener('mousedown', function (e) { + if (hasCls(t$(e), 'resize')) { + e.preventDefault(); + $resize = closest$(e, 'td'); + } else if (hasCls(t$(e), 'h-resize')) { + e.preventDefault(); + $hresize = closest$(e, 'section').querySelector('article'); + } + $prevPanel = $panel() || $prevPanel; + let section = closest$(e, 'section'); + section && setActive(section); + let column = closest$(e, '.column'); + if (column && (!e.altKey && !e.metaKey && !e.ctrlKey)) { + togCls('active', '.column', column); + } + }); + + document.addEventListener('mousemove', function (e) { + if ($resize) $resize.width = e.clientX; + else if ($hresize) { + const $sib = $hresize.closest('section').nextSibling; + const rect = $hresize.getBoundingClientRect(); + let h = e.y - rect.top < 0 ? 0 : e.y - rect.top; + let diff = h - rect.height; + if ($sib && $sib.nextSibling) { + const $sArt = $sib.querySelector('article'); + const sRect = $sArt.getBoundingClientRect(); + if (sRect.height - diff <= 0) { + $hresize.style.height = `${+$hresize.style.height.split('px')[0] + sRect.height}px`; + $sArt.style.height = `0px`; + return; + } + $sArt.style.height = `${sRect.height - diff}px`; + } + $hresize.style.height = `${h}px`; + } + }) + + document.addEventListener('mouseup', () => { + $resize = null; + $hresize = null; + }); + + document.addEventListener('click', function (e) { + // @formatter:off + switch (t$(e).classList[0]) { + case 'new': menuNew(); return; + case 'ls': menuLs(); return; + case 'reset': menuReset(); return; + case 'save': menuSave(); return; + case 'mv': menuMv(); return; + case 'del': menuDel(); return; + case 'settings': menuSettings(); return; + case 'run': menuRun(); return; + case 'export': menuExport(); return; + case 'close': paneClose(e); return; + case 'max': paneMax(e); return; + case 'move': paneMove(e); return; + case 'back': paneHist(hist().back, hist().forward)(e); return; + case 'forward': paneHist(hist().forward, hist().back)(e); return; + } + // @formatter:on + if (isChordNavBlank(e) && e.button === 0) { + link($content()) && navBlank(link($content())); + } else if (isChordNav(e) && e.button === 0) { + let name = link($content()); + if (name) { + setActive($prevPanel); + load(name); + $content().focus(); + } + } + }); + + window.addEventListener('beforeunload', e => { + if ($('.dirty')) { + e.preventDefault(); + e.returnValue = true; + } + }) + + function offset(text) { + const stack = []; + let o = 0; + text.split('').forEach((c, i) => { + if (c === '(') stack.push({spaces: i - o, total: i}); + else if (c === ')') stack.pop(); + else if (c === '\n') o = i + 1; + }); + return stack.pop() || 0; + } + + const move = (e, pos) => e.setSelectionRange(pos, pos); + const caret = e => e.value.substring(0, e.selectionStart); + let last; + + document.addEventListener('keydown', function (e) { + if (e.key === 'Enter') { + if (t$(e).id === 'search') { + handleSearch(e); + } else if (isChordNav(e)) { + $search().value = ''; + $search().classList.add('searching'); + $search().focus(); + } else if (hasCls(e.target, 'name')) { + load($name().value); + } else { + e.preventDefault(); + let s = offset(caret(t$(e))); + if (s.spaces === undefined) write('\n') + else write('\n' + Array(s.spaces + 3).join(' ')); + } + } else if (e.key === 'Escape' && e.target.id === 'search') { + $search().classList.remove('searching'); + $content().focus(); + } else if (t$(e).classList[0] === 'content') { + const textarea = t$(e); + if (e.key === '(') { + e.preventDefault(); + write('()'); + move(textarea, textarea.selectionStart - 1); + } + if (e.key === '\'') { + e.preventDefault(); + write('(quote )'); + move(textarea, textarea.selectionStart - 1); + } else if (e.key === 'Tab') { + e.preventDefault(); + let c = caret(textarea); + if (c[c.length - 1] === '(') { + return move(textarea, last); + } + let o = offset(c); + if (o.spaces === undefined) return; + last = c.length; + move(textarea, o.total + 1); + } else if (e.key === 'r' && (e.metaKey || e.ctrlKey)) { + runPane(textarea.value); + } + } + }); + + document.getElementById('import').addEventListener('change', function (e) { + const file = e.target.files[0]; + const r = new FileReader(); + r.onload = e => { + let data = JSON.parse('' + e.target.result); + Object.keys(data).forEach(k => storeSet(k, data[k])); + refresh(); + } + r.readAsText(file); + refresh(); + }); + + document.addEventListener('keyup', function (e) { + if (t$(e).className === 'content') { + let $parse = closest$(e, 'section').querySelector('.parse'); + try { + parse($content().value); + $parse.innerHTML = ''; + } catch (e) { + $parse.innerHTML = e; + } + } + }) + + refresh(); + createPane(); + load((location.hash && location.hash.substring(1)) || 'main'); +</script> +</body> +</html> \ No newline at end of file diff --git a/html/tuner/index.html b/html/tuner/index.html new file mode 100644 index 0000000..20bf125 --- /dev/null +++ b/html/tuner/index.html @@ -0,0 +1,193 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Tuner</title> + <style> + * { + box-sizing: border-box; + margin: 0; + padding: 0; + } + body { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + margin: 0; + font-family: Arial, sans-serif; + background-color: beige; + } + canvas { + border: none; + width: 100%; + max-width: 100%; + height: auto; + } + #note { + margin-top: 10px; + font-size: 18px; + font-weight: bold; + text-align: center; + } + #buttons { + display: flex; + flex-wrap: wrap; + justify-content: center; + margin-top: 10px; + } + button { + margin: 5px; + padding: 10px; + font-size: 16px; + flex: 1 1 25%; + max-width: 100px; + } + @media (max-width: 600px) { + button { + flex: 1 1 33.33%; + } + } + </style> +</head> +<body> + + <canvas id="waveformCanvas"></canvas> + <div id="note">Note: N/A</div> + + <div id="buttons"></div> + + <script> + const canvas = document.getElementById('waveformCanvas'); + const canvasCtx = canvas.getContext('2d'); + const noteDisplay = document.getElementById('note'); + + let audioContext; + let analyzer; + let dataArray; + let bufferLength; + + // Note frequencies in Hz for A4 = 440Hz + const notesFrequencies = { + 'C': 261.63, + 'C#': 277.18, + 'D': 293.66, + 'D#': 311.13, + 'E': 329.63, + 'F': 349.23, + 'F#': 369.99, + 'G': 392.00, + 'G#': 415.30, + 'A': 440.00, + 'A#': 466.16, + 'B': 493.88 + }; + + navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => { + audioContext = new (window.AudioContext || window.webkitAudioContext)(); + const source = audioContext.createMediaStreamSource(stream); + + analyzer = audioContext.createAnalyser(); + analyzer.fftSize = 2048; + bufferLength = analyzer.frequencyBinCount; + dataArray = new Uint8Array(bufferLength); + + source.connect(analyzer); + drawWaveform(); + setInterval(detectNote, 500); + }).catch(err => { + console.error('Error accessing the microphone:', err); + }); + + function resizeCanvas() { + canvas.width = window.innerWidth - 20; + canvas.height = window.innerHeight / 4; + } + window.addEventListener('resize', resizeCanvas); + resizeCanvas(); + + function drawWaveform() { + requestAnimationFrame(drawWaveform); + + analyzer.getByteTimeDomainData(dataArray); + + canvasCtx.clearRect(0, 0, canvas.width, canvas.height); + canvasCtx.beginPath(); + + const sliceWidth = canvas.width / bufferLength; + let x = 0; + + for (let i = 0; i < bufferLength; i++) { + const v = dataArray[i] / 128.0; + const y = v * canvas.height / 2; + + if (i === 0) { + canvasCtx.moveTo(x, y); + } else { + canvasCtx.lineTo(x, y); + } + + x += sliceWidth; + } + + canvasCtx.lineTo(canvas.width, canvas.height / 2); + canvasCtx.stroke(); + } + + // Detect the dominant frequency and map it to a musical note + function detectNote() { + const freqArray = new Float32Array(analyzer.frequencyBinCount); + analyzer.getFloatFrequencyData(freqArray); + + let maxAmp = -Infinity; + let maxIndex = 0; + + for (let i = 0; i < freqArray.length; i++) { + if (freqArray[i] > maxAmp) { + maxAmp = freqArray[i]; + maxIndex = i; + } + } + + // Nyquist frequency is half the sample rate of a signal + const nyquist = audioContext.sampleRate / 2; + const frequency = maxIndex * nyquist / freqArray.length; + + const note = getNoteFromFrequency(frequency); + noteDisplay.textContent = `Note: ${note}`; + } + + // Convert frequency to musical note + function getNoteFromFrequency(frequency) { + const notes = [ + 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B' + ]; + const A4 = 440; + const semitoneRatio = Math.pow(2, 1 / 12); + const noteIndex = Math.round(12 * Math.log2(frequency / A4)); + const note = notes[(noteIndex % 12 + 12) % 12]; + + return frequency ? note : 'N/A'; + } + + const buttonsContainer = document.getElementById('buttons'); + Object.keys(notesFrequencies).forEach(note => { + const button = document.createElement('button'); + button.textContent = note; + button.onclick = () => playNote(notesFrequencies[note]); + buttonsContainer.appendChild(button); + }); + + function playNote(frequency) { + const osc = audioContext.createOscillator(); + osc.type = 'sine'; // Valid values include 'sine', 'square', 'triangle', 'sawtooth' + osc.frequency.setValueAtTime(frequency, audioContext.currentTime); // Frequency in Hz + osc.connect(audioContext.destination); + osc.start(); + osc.stop(audioContext.currentTime + 1); // Play the note for 1 second + } + </script> +</body> +</html> |