diff options
-rwxr-xr-x | bash/acmetodo | 145 | ||||
-rwxr-xr-x | bash/acmetodo-add | 25 | ||||
-rwxr-xr-x | bash/acmetodo-all | 17 | ||||
-rwxr-xr-x | bash/acmetodo-done | 19 | ||||
-rw-r--r-- | bash/acmetodo-filter | 22 | ||||
-rwxr-xr-x | bash/acmetodo-inprogress | 19 | ||||
-rwxr-xr-x | bash/acmetodo-todo | 22 | ||||
-rwxr-xr-x | bash/acmetodo-toggle | 56 | ||||
-rw-r--r-- | elm/cost-of-meeting/src/Main.elm | 1 | ||||
-rw-r--r-- | elm/cyoa/elm-stuff/0.19.1/d.dat | bin | 1554 -> 1594 bytes | |||
-rw-r--r-- | html/merfolk/app.js | 26 | ||||
-rw-r--r-- | html/merfolk/index.html | 2 | ||||
-rw-r--r-- | html/playground/index.html | 435 | ||||
-rw-r--r-- | js/MAP.md | 3 | ||||
-rw-r--r-- | js/seed/README.md | 91 | ||||
-rw-r--r-- | js/seed/index.html | 15 | ||||
-rw-r--r-- | js/seed/src/api.js | 15 | ||||
-rw-r--r-- | js/seed/src/app.js | 165 | ||||
-rw-r--r-- | js/seed/src/dev.js | 74 | ||||
-rw-r--r-- | js/seed/src/state.js | 13 | ||||
-rw-r--r-- | js/seed/src/update.js | 22 | ||||
-rw-r--r-- | js/seed/src/view.js | 62 | ||||
-rw-r--r-- | js/seed/style.css | 82 | ||||
-rwxr-xr-x | perl/functional/main.pl | 227 | ||||
-rw-r--r-- | ts/thinking-about-unions/left-pad.ts | 27 |
25 files changed, 1340 insertions, 245 deletions
diff --git a/bash/acmetodo b/bash/acmetodo new file mode 100755 index 0000000..0c0b72f --- /dev/null +++ b/bash/acmetodo @@ -0,0 +1,145 @@ +#!/bin/sh +# acmetodo: Main script to open and manage the todo list in Acme. + +PLAN9=${PLAN9:-"/Users/eli/plan9"} # Ensure this is your correct PLAN9 path +TODO_FILE="$PLAN9/lib/todo" +ACME_BIN="$PLAN9/bin/acme" +STARTPLUMB_BIN="$PLAN9/bin/startplumb" +ACME_FS_ROOT="/acme" # Standard Plan 9 /acme FUSE mount point + +# --- DEBUGGING START --- +DEBUG_LOG="/tmp/acmetodo_debug.log" +rm -f "$DEBUG_LOG" # Clear previous log on each run + +log_debug() { + echo "$(date '+%Y-%m-%d %H:%M:%S') [DEBUG] $@" >> "$DEBUG_LOG" + echo "$(date '+%H:%M:%S') [DEBUG] $@" >/dev/stderr +} + +log_error() { + echo "$(date '+%Y-%m-%d %H:%M:%S') [ERROR] $@" >> "$DEBUG_LOG" + echo "$(date '+%H:%M:%S') [ERROR] $@" >/dev/stderr +} + +log_debug "Script started." +log_debug "PLAN9 is $PLAN9" +log_debug "TODO_FILE is $TODO_FILE" +log_debug "ACME_BIN is $ACME_BIN" +log_debug "STARTPLUMB_BIN is $STARTPLUMB_BIN" +log_debug "Current \$winid (from Acme environment) is '$winid'" +# --- DEBUGGING END --- + +# --- PLUMBER CHECK AND LAUNCH --- +log_debug "Checking if plumber is running..." +if ! ps aux | grep -q "[p]lumber"; then + log_debug "Plumber not found. Attempting to start plumber..." + if [ -x "$STARTPLUMB_BIN" ]; then + "$STARTPLUMB_BIN" & # Launch plumber in the background + log_debug "Plumber launch command issued. Waiting for plumber to initialize..." + sleep 1 # Give plumber a moment to fully start + else + log_error "startplumb executable not found at '$STARTPLUMB_BIN'. Cannot start plumber." + exit 1 + fi +else + log_debug "Plumber is already running." +fi +# --- END PLUMBER CHECK --- + + +# Ensure the lib directory exists +mkdir -p "$(dirname "$TODO_FILE")" +# Ensure the todo file exists +touch "$TODO_FILE" + +# Function to safely get window ID by name, suppressing stderr from acme -w +get_existing_win_id() { + # Pipe stderr of acme -w to /dev/null to suppress the "usage" message + "$ACME_BIN" -w 2>/dev/null | grep "^$TODO_FILE " | cut -d' ' -f1 | head -n 1 +} + + +# Function to update a window's content, name, and tag +update_todo_window() { + local win_id="$1" + log_debug "Function update_todo_window called for win_id: $win_id" + + # 1. Set the window's name + log_debug "Attempting to set window $win_id name to '$TODO_FILE'" + echo "name $TODO_FILE" | "$ACME_BIN" -a "$win_id" + if [ $? -ne 0 ]; then + log_error "Failed to send 'name' command to window $win_id." + fi + sleep 0.05 # Small delay to ensure command is processed + + # 2. Set the window's tag + log_debug "Attempting to set window $win_id tag." + echo 'tag Get Put | Add | Todo InProgress Done All | Quit' | "$ACME_BIN" -a "$win_id" + if [ $? -ne 0 ]; then + log_error "Failed to send 'tag' command to window $win_id." + fi + sleep 0.05 # Small delay + + # 3. Load content directly into the window's body + log_debug "Attempting to write content to window $win_id body." + if [ -f "$TODO_FILE" ]; then + log_debug "Reading content from '$TODO_FILE' and piping to $win_id body." + cat "$TODO_FILE" | "$ACME_BIN" -a "$win_id" body + if [ $? -ne 0 ]; then + log_error "Failed to write content to window $win_id body from $TODO_FILE." + fi + else + log_debug "TODO_FILE '$TODO_FILE' does not exist or is not readable. Writing initial content." + echo "## Acme To-Do List" | "$ACME_BIN" -a "$win_id" body + if [ $? -ne 0 ]; then + log_error "Failed to write initial content to window $win_id body." + fi + fi + echo 'clean' | "$ACME_BIN" -a "$win_id" # Mark window as clean after setting content + log_debug "Finished updating window $win_id." +} + + +# --- MAIN LOGIC --- +if [ -n "$winid" ]; then + log_debug "Running in Case 1: Script called from an existing Acme window." + update_todo_window "$winid" +else + # Always try to find existing window first + EXISTING_TODO_WINID=$(get_existing_win_id) + log_debug "EXISTING_TODO_WINID (found by checking existing Acme windows) is '$EXISTING_TODO_WINID'" + + if [ -n "$EXISTING_TODO_WINID" ]; then + log_debug "Running in Case 2: Script called from terminal, existing todo window found." + update_todo_window "$EXISTING_TODO_WINID" + else + log_debug "Running in Case 3: Script called from terminal, new todo window needed." + + # Capture the highest window ID *before* opening a new one + PRE_NEW_WIN_ID=$(ls -d "$ACME_FS_ROOT"/[0-9]* 2>/dev/null | sort -V | tail -n 1 | xargs basename) + log_debug "Highest existing Acme window ID before new creation: '$PRE_NEW_WIN_ID'" + + # 1. Open a new, empty Acme window. Don't try to capture its ID directly, as it's unreliable. + log_debug "Attempting to create a new Acme window with '$ACME_BIN -l /dev/null'." + "$ACME_BIN" -l /dev/null 2>/dev/null & # Run in background and suppress stderr + + # Give Acme a moment to open the window + sleep 0.5 + + # 2. Find the ID of the newly created window (should be the highest now) + NEW_WIN_ID=$(ls -d "$ACME_FS_ROOT"/[0-9]* 2>/dev/null | sort -V | tail -n 1 | xargs basename) + log_debug "Highest existing Acme window ID after new creation: '$NEW_WIN_ID'" + + if [ -z "$NEW_WIN_ID" ] || [ "$NEW_WIN_ID" = "$PRE_NEW_WIN_ID" ]; then + log_error "Failed to create a new Acme window or get its ID correctly." + log_error "Possible causes: Acme not running, or its communication with plumber failed, or the FUSE mount is inaccessible." + exit 1 + fi + + # 3. Call the update function for the new window + log_debug "Calling update_todo_window for the newly created ID: $NEW_WIN_ID" + update_todo_window "$NEW_WIN_ID" + fi +fi + +log_debug "Script finished." \ No newline at end of file diff --git a/bash/acmetodo-add b/bash/acmetodo-add new file mode 100755 index 0000000..b40663d --- /dev/null +++ b/bash/acmetodo-add @@ -0,0 +1,25 @@ +#!/bin/sh +# acmetodo-add: Adds a new todo item. + +PLAN9=${PLAN9:-"/usr/local/plan9"} +TODO_FILE="$PLAN9/lib/todo" +ACME_BIN="$PLAN9/bin/acme" + +# Prompt for subject in Acme's current window (where the command was issued) +# Output to stderr so it doesn't interfere with potential stdout pipe. +echo -n "Subject: " >/dev/stderr +read subject + +if [ -n "$subject" ]; then + echo "[ ] $subject" >> "$TODO_FILE" + # Find the winid of the main acmetodo window to refresh it. + # This assumes the main acmetodo window's name is the TODO_FILE path. + MAIN_TODO_WINID=$($ACME_BIN -w | grep "^$TODO_FILE" | cut -d' ' -f1 | head -n 1) + if [ -n "$MAIN_TODO_WINID" ]; then + echo 'Get' | $ACME_BIN -a "$MAIN_TODO_WINID" + else + echo "Warning: Main acmetodo window not found to refresh." >/dev/stderr + fi +else + echo "No subject provided. Item not added." >/dev/stderr +fi \ No newline at end of file diff --git a/bash/acmetodo-all b/bash/acmetodo-all new file mode 100755 index 0000000..c00bb9b --- /dev/null +++ b/bash/acmetodo-all @@ -0,0 +1,17 @@ +#!/bin/sh +# acmetodo-all: Shows all items in the todo list. + +PLAN9=${PLAN9:-"/usr/local/plan9"} +TODO_FILE="$PLAN9/lib/todo" +ACME_BIN="$PLAN9/bin/acme" + +MAIN_TODO_WINID=$($ACME_BIN -w | grep "^$TODO_FILE" | cut -d' ' -f1 | head -n 1) + +if [ -z "$MAIN_TODO_WINID" ]; then + echo "Error: Main acmetodo window not found." >/dev/stderr + exit 1 +fi + +# Simply get (reload) the content of the todo file +echo 'Get' | $ACME_BIN -a "$MAIN_TODO_WINID" +echo 'clean' | $ACME_BIN -a "$MAIN_TODO_WINID" \ No newline at end of file diff --git a/bash/acmetodo-done b/bash/acmetodo-done new file mode 100755 index 0000000..4829331 --- /dev/null +++ b/bash/acmetodo-done @@ -0,0 +1,19 @@ +#!/bin/sh +# acmetodo-done: Filters the todo list to show only 'done' items. + +PLAN9=${PLAN9:-"/usr/local/plan9"} +TODO_FILE="$PLAN9/lib/todo" +ACME_BIN="$PLAN9/bin/acme" + +MAIN_TODO_WINID=$($ACME_BIN -w | grep "^$TODO_FILE" | cut -d' ' -f1 | head -n 1) + +if [ -z "$MAIN_TODO_WINID" ]; then + echo "Error: Main acmetodo window not found." >/dev/stderr + exit 1 +fi + +filtered_content=$(grep '^[x] ' "$TODO_FILE") + +echo 'data' | $ACME_BIN -a "$MAIN_TODO_WINID" +echo "$filtered_content" | $ACME_BIN -a "$MAIN_TODO_WINID" +echo 'clean' | $ACME_BIN -a "$MAIN_TODO_WINID" \ No newline at end of file diff --git a/bash/acmetodo-filter b/bash/acmetodo-filter new file mode 100644 index 0000000..6149207 --- /dev/null +++ b/bash/acmetodo-filter @@ -0,0 +1,22 @@ +#!/bin/sh +# acmetodo-todo: Filters the todo list to show only 'to-do' items. + +PLAN9=${PLAN9:-"/usr/local/plan9"} +TODO_FILE="$PLAN9/lib/todo" +ACME_BIN="$PLAN9/bin/acme" + +# Find the winid of the main acmetodo window +MAIN_TODO_WINID=$($ACME_BIN -w | grep "^$TODO_FILE" | cut -d' ' -f1 | head -n 1) + +if [ -z "$MAIN_TODO_WINID" ]; then + echo "Error: Main acmetodo window not found." >/dev/stderr + exit 1 +fi + +# Filter the content and send it back to the window +filtered_content=$(grep '^[ ] ' "$TODO_FILE") + +# Clear current window content and then write the filtered content +echo 'data' | $ACME_BIN -a "$MAIN_TODO_WINID" +echo "$filtered_content" | $ACME_BIN -a "$MAIN_TODO_WINID" +echo 'clean' | $ACME_BIN -a "$MAIN_TODO_WINID" # Mark window as clean after update \ No newline at end of file diff --git a/bash/acmetodo-inprogress b/bash/acmetodo-inprogress new file mode 100755 index 0000000..d5ea505 --- /dev/null +++ b/bash/acmetodo-inprogress @@ -0,0 +1,19 @@ +#!/bin/sh +# acmetodo-inprogress: Filters the todo list to show only 'in progress' items. + +PLAN9=${PLAN9:-"/usr/local/plan9"} +TODO_FILE="$PLAN9/lib/todo" +ACME_BIN="$PLAN9/bin/acme" + +MAIN_TODO_WINID=$($ACME_BIN -w | grep "^$TODO_FILE" | cut -d' ' -f1 | head -n 1) + +if [ -z "$MAIN_TODO_WINID" ]; then + echo "Error: Main acmetodo window not found." >/dev/stderr + exit 1 +fi + +filtered_content=$(grep '^[>] ' "$TODO_FILE") + +echo 'data' | $ACME_BIN -a "$MAIN_TODO_WINID" +echo "$filtered_content" | $ACME_BIN -a "$MAIN_TODO_WINID" +echo 'clean' | $ACME_BIN -a "$MAIN_TODO_WINID" \ No newline at end of file diff --git a/bash/acmetodo-todo b/bash/acmetodo-todo new file mode 100755 index 0000000..6149207 --- /dev/null +++ b/bash/acmetodo-todo @@ -0,0 +1,22 @@ +#!/bin/sh +# acmetodo-todo: Filters the todo list to show only 'to-do' items. + +PLAN9=${PLAN9:-"/usr/local/plan9"} +TODO_FILE="$PLAN9/lib/todo" +ACME_BIN="$PLAN9/bin/acme" + +# Find the winid of the main acmetodo window +MAIN_TODO_WINID=$($ACME_BIN -w | grep "^$TODO_FILE" | cut -d' ' -f1 | head -n 1) + +if [ -z "$MAIN_TODO_WINID" ]; then + echo "Error: Main acmetodo window not found." >/dev/stderr + exit 1 +fi + +# Filter the content and send it back to the window +filtered_content=$(grep '^[ ] ' "$TODO_FILE") + +# Clear current window content and then write the filtered content +echo 'data' | $ACME_BIN -a "$MAIN_TODO_WINID" +echo "$filtered_content" | $ACME_BIN -a "$MAIN_TODO_WINID" +echo 'clean' | $ACME_BIN -a "$MAIN_TODO_WINID" # Mark window as clean after update \ No newline at end of file diff --git a/bash/acmetodo-toggle b/bash/acmetodo-toggle new file mode 100755 index 0000000..bffccec --- /dev/null +++ b/bash/acmetodo-toggle @@ -0,0 +1,56 @@ +#!/bin/sh +# acmetodo-toggle: Toggles the status of a selected todo item. + +PLAN9=${PLAN9:-"/usr/local/plan9"} +TODO_FILE="$PLAN9/lib/todo" +ACME_BIN="$PLAN9/bin/acme" + +# Read the selected line(s) from standard input (Acme pipes the selection) +selected_lines=$(cat) + +if [ -z "$selected_lines" ]; then + echo "No line selected to toggle." >/dev/stderr + exit 1 +fi + +# Process only the first line of the selection for toggling +line_to_toggle=$(echo "$selected_lines" | head -n 1) + +new_line="" +case "$line_to_toggle" in + '[] '*) + new_line="[>] ${line_to_toggle:3}" # Extract content after '[ ]' + ;; + '[>] '*) + new_line="[x] ${line_to_toggle:3}" # Extract content after '[>]' + ;; + '[x] '*) + new_line="[ ] ${line_to_toggle:3}" # Extract content after '[x]' + ;; + *) + echo "Warning: Selected line does not match a known todo item format: $line_to_toggle" >/dev/stderr + exit 0 # Exit gracefully if not a todo item + ;; +esac + +if [ -n "$new_line" ]; then + # Use awk for a robust in-place replacement. + # It reads the entire file, replaces the first exact match, then prints. + awk -v old_line="$line_to_toggle" -v new_line="$new_line" ' + BEGIN { replaced = 0 } + $0 == old_line && !replaced { + print new_line + replaced = 1 + next + } + { print } + ' "$TODO_FILE" > "${TODO_FILE}.tmp" && mv "${TODO_FILE}.tmp" "$TODO_FILE" + + # Find the winid of the main acmetodo window to refresh it. + MAIN_TODO_WINID=$($ACME_BIN -w | grep "^$TODO_FILE" | cut -d' ' -f1 | head -n 1) + if [ -n "$MAIN_TODO_WINID" ]; then + echo 'Get' | $ACME_BIN -a "$MAIN_TODO_WINID" + else + echo "Warning: Main acmetodo window not found to refresh." >/dev/stderr + fi +fi \ No newline at end of file diff --git a/elm/cost-of-meeting/src/Main.elm b/elm/cost-of-meeting/src/Main.elm index 864515b..fdc16d5 100644 --- a/elm/cost-of-meeting/src/Main.elm +++ b/elm/cost-of-meeting/src/Main.elm @@ -6,7 +6,6 @@ import Html.Attributes as Attr import Html.Events exposing (onInput) import String - -- MODEL type alias Model = diff --git a/elm/cyoa/elm-stuff/0.19.1/d.dat b/elm/cyoa/elm-stuff/0.19.1/d.dat index 7223730..7587e5b 100644 --- a/elm/cyoa/elm-stuff/0.19.1/d.dat +++ b/elm/cyoa/elm-stuff/0.19.1/d.dat Binary files differdiff --git a/html/merfolk/app.js b/html/merfolk/app.js index cb6eccf..963f22d 100644 --- a/html/merfolk/app.js +++ b/html/merfolk/app.js @@ -1,11 +1,11 @@ /** - * Mermaid diagram editor and viewer application - * Features: - * - Real-time preview of Mermaid diagrams - * - Pan and zoom functionality - * - Export as PNG with configurable scale - * - Export as SVG - * - Reset view to center + * Merfolk is a mermaid diagram editor and viewer. + * + * Dependencies: + * - mermaid.js + * - html2canvas + * - panzoom + * */ // Configuration @@ -15,7 +15,7 @@ mermaid.initialize({ securityLevel: 'loose' }); -// DOM Elements +// Known DOM Elements const input = document.getElementById('mermaid-input'); const preview = document.getElementById('mermaid-preview'); const errorMessage = document.getElementById('error-message'); @@ -45,7 +45,7 @@ const debounce = (func, wait) => { }; /** - * Renders a Mermaid diagram from the provided text + * Renders a Mermaid diagram from the provided markup * Handles initialization of panzoom functionality * @param {string} text - The Mermaid diagram syntax * @returns {Promise<void>} @@ -96,7 +96,7 @@ const handleReset = () => { /** * Exports the current diagram as a PNG image - * Prompts user to select scale factor before export + * Prompts user to select a desired scale before exporting * @returns {Promise<void>} */ const handleExport = async () => { @@ -202,7 +202,7 @@ const handleExport = async () => { document.body.appendChild(overlay); }); - // If user cancelled, return early + // Return early on cancel if (scale === null) return; const canvas = await html2canvas(preview, { @@ -233,7 +233,6 @@ const handleExportSvg = async () => { // Reset view before capturing handleReset(); - // Small delay to ensure the reset is complete await new Promise(resolve => setTimeout(resolve, 100)); const svg = preview.querySelector('svg'); @@ -255,7 +254,6 @@ const handleExportSvg = async () => { link.href = svgUrl; link.click(); - // Clean up URL.revokeObjectURL(svgUrl); } catch (err) { errorMessage.textContent = 'SVG export failed.'; @@ -263,7 +261,7 @@ const handleExportSvg = async () => { } }; -// Event Handlers + const debouncedRender = debounce(renderMermaid, 300); input.addEventListener('input', (e) => { debouncedRender(e.target.value); diff --git a/html/merfolk/index.html b/html/merfolk/index.html index f184e07..4d30da7 100644 --- a/html/merfolk/index.html +++ b/html/merfolk/index.html @@ -3,7 +3,7 @@ <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>Mermaid Preview</title> + <title>Merfolk Mermaid Diagram Viewer</title> <script src="mermaid.min.js"></script> <link rel="stylesheet" href="styles.css"> </head> diff --git a/html/playground/index.html b/html/playground/index.html index 680f022..a236d2f 100644 --- a/html/playground/index.html +++ b/html/playground/index.html @@ -6,238 +6,243 @@ <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; - } + html, body { height: 100%; margin: 0; padding: 0; } + body { display: flex; flex-direction: column; background-color: #ddd; } + #app { display: flex; flex-direction: column; padding: 10px; height: 100%; box-sizing: border-box; } + .button-container { width: 100%; display: flex; justify-content: flex-end; margin-bottom: 10px; flex-shrink: 0; } + 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:disabled { background-color: #999; cursor: not-allowed; } + button.run { background-color: teal; color: #FFFFFF; } + 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; flex-shrink: 0; } + 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; flex-grow: 1; } + #console .log-item { margin-bottom: 5px; border-bottom: 1px solid #333; } + #console details { cursor: pointer; } + #console summary { outline: none; } </style> </head> <body> + <div id="app"></div> - <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> + <iframe id="sandbox" style="display: none;" title="JavaScript Sandbox"></iframe> <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); + /** + * ---------------------------------------------------------------- + * Functional Utilities + * ---------------------------------------------------------------- + */ + + const pipe = (...fns) => (initialValue) => fns.reduce((acc, fn) => fn(acc), initialValue); + const compose = (...fns) => (initialValue) => fns.reduceRight((acc, fn) => fn(acc), initialValue); + + /** + * ---------------------------------------------------------------- + * The Elm Architecture (Model, View, Update) Implementation + * ---------------------------------------------------------------- + */ + + const init = { + code: ` + const add = (a, b) => a + b; + const multiply = (a, b) => a * b; + + const addAndMultiply = pipe(add, multiply); + + console.log(addAndMultiply(2, 3)); +`, + consoleOutput: [], + sandboxReady: false, + }; + + const view = (model) => ` + <div class="button-container"> + <button onclick="dispatch({ type: 'CLEAR' })">Clear</button> + <button onclick="dispatch({ type: 'DOWNLOAD' })">Download</button> + <button onclick="dispatch({ type: 'SHARE' })">Share</button> + <button onclick="dispatch({ type: 'RUN_CODE' })" class="run" ${!model.sandboxReady ? 'disabled title="Sandbox is loading..."' : ''}>Run Code</button> + </div> + <textarea id="codeInput" oninput="dispatch({ type: 'CODE_CHANGED', payload: this.value })" onkeydown="handleKeyDown(event)">${model.code}</textarea> + <div id="console"> + ${model.consoleOutput.map(log => { + if (typeof log === 'object' && log !== null) { + return `<div class="log-item"><details><summary>${Object.prototype.toString.call(log)}</summary><pre>${JSON.stringify(log, null, 2)}</pre></details></div>`; + } + return `<div class="log-item">${log}</div>`; + }).join('')} + </div> + `; + + const update = (msg, model) => { + switch (msg.type) { + case 'CODE_CHANGED': + return { ...model, code: msg.payload }; + + case 'RUN_CODE': + return model; + + case 'CONSOLE_LOG': + return { ...model, consoleOutput: [...model.consoleOutput, ...msg.payload] }; + + case 'CLEAR': + if (confirm('Are you sure you want to reset the playground?')) { + window.location.hash = ''; + window.location.reload(); + } + return model; + + case 'CLEAR_CONSOLE': + return { ...model, consoleOutput: [] }; + + case 'SHARE': + case 'DOWNLOAD': + return model; + + case 'SANDBOX_READY': + return { ...model, sandboxReady: true }; + + default: + return model; } - - // 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(); + }; + + /** + * ---------------------------------------------------------------- + * Application Core & Side-Effect Handling + * ---------------------------------------------------------------- + */ + + let model = null; + const appContainer = document.getElementById('app'); + const sandbox = document.getElementById('sandbox'); + + const dispatch = (msg) => { + const newModel = update(msg, model); + handleSideEffects(msg, newModel); + model = newModel; + render(model); + }; + + const handleSideEffects = (msg, model) => { + if (msg.type === 'RUN_CODE') { + if (!model.sandboxReady) return; + dispatch({ type: 'CLEAR_CONSOLE' }); + sandbox.contentWindow.postMessage({ code: model.code }, '*'); + } else if (msg.type === 'SHARE') { + const encodedCode = btoa(encodeURIComponent(model.code)); + window.location.hash = encodedCode; + window.prompt("Copy the URL to share.", window.location.href); + } else if (msg.type === 'DOWNLOAD') { + const htmlContent = document.documentElement.outerHTML.replace( + /<textarea id="codeInput"[^>]*>.*<\/textarea>/, + `<textarea id="codeInput">${model.code}</textarea>` + ); + const blob = new Blob([htmlContent], { type: 'text/html' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'playground.html'; + a.click(); + URL.revokeObjectURL(url); } - } - - 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); + }; + + const render = (model) => { + const focusedElementId = document.activeElement?.id; + const selectionStart = document.activeElement?.selectionStart; + const selectionEnd = document.activeElement?.selectionEnd; + + appContainer.innerHTML = view(model); + + if (focusedElementId === 'codeInput') { + const focusedElement = document.getElementById(focusedElementId); + if (focusedElement) { + focusedElement.focus(); + if (typeof selectionStart === 'number') { + focusedElement.selectionStart = selectionStart; + focusedElement.selectionEnd = selectionEnd; + } } } - } + }; - 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; + const handleKeyDown = (event) => { + if (event.key === 'Tab') { + event.preventDefault(); + const start = event.target.selectionStart; + const end = event.target.selectionEnd; + event.target.value = event.target.value.substring(0, start) + " " + event.target.value.substring(end); + dispatch({ type: 'CODE_CHANGED', payload: event.target.value }); + event.target.selectionStart = event.target.selectionEnd = start + 2; } + if (event.metaKey && event.key === 'Enter') { + event.preventDefault(); + dispatch({ type: 'RUN_CODE' }); + } + }; + + /** + * ---------------------------------------------------------------- + * Sandbox Initialization + * ---------------------------------------------------------------- + */ + + const sandboxSrc = ` + <script> + const pipe = ${pipe.toString()}; + const compose = ${compose.toString()}; + + const originalConsoleLog = console.log; + console.log = (...args) => { + parent.postMessage({ type: 'CONSOLE_LOG', payload: args }, '*'); + }; + + window.addEventListener('message', (event) => { + try { + eval(event.data.code); + } catch (e) { + console.log(e.toString()); + } + }); + <\/script> + `; - if (playground.innerHTML.trim() !== "") { - playground.innerHTML = ""; + window.addEventListener('message', (event) => { + if (event.source === sandbox.contentWindow && event.data.type === 'CONSOLE_LOG') { + dispatch(event.data); } - mountFunction(playground); + }); + + /** + * ---------------------------------------------------------------- + * Application Entry Point + * ---------------------------------------------------------------- + */ + + const hash = window.location.hash.substring(1); + if (hash) { + try { + const decodedCode = decodeURIComponent(atob(hash)); + model = { ...init, code: decodedCode }; + } catch (error) { + console.error('Failed to decode the URL hash:', error); + model = init; + } + } else { + model = init; } + render(model); - document.getElementById('codeInput').addEventListener('keydown', function(event) { - if (event.metaKey && event.key === 'Enter') { - event.preventDefault(); - evaluateCode(); - } - }); + sandbox.onload = () => { + dispatch({ type: 'SANDBOX_READY' }); + }; + + sandbox.srcdoc = sandboxSrc; - window.onload = loadCodeFromHash; </script> </body> -</html> +</html> \ No newline at end of file diff --git a/js/MAP.md b/js/MAP.md index 7de3f62..f1eb64b 100644 --- a/js/MAP.md +++ b/js/MAP.md @@ -37,6 +37,7 @@ [rotjs](https://ondras.github.io/rot.js/hp/) - `sand`, a sand simulation - `scripting-lang`, maybe I should make my own scripting language? +- `seed`, starter kit for new, simple javascript web applications - `ship-game`, what Avi has dubbed "LAZER-GATE-1" - `starting-place`, another/different attempt at making a generic starting point for HTML and JS games @@ -46,4 +47,4 @@ canvas - `toy-llm`, exactly what it says on the box, the most basic of toy LLM implementations -- `where`, is a simple `where` function implemented in JavaScript. Useful for easy cases in the browser \ No newline at end of file +- `where`, is a simple `where` function implemented in JavaScript. Useful for easy cases in the browser diff --git a/js/seed/README.md b/js/seed/README.md new file mode 100644 index 0000000..8159cb3 --- /dev/null +++ b/js/seed/README.md @@ -0,0 +1,91 @@ +# Seed: Minimal FRP/TEA Web App Starter Kit + +This is an opinionated, minimal starting point for browser-native web apps using a functional, Elm-style architecture (FRP/TEA) and only browser APIs. No frameworks, no build step, just ES modules. + +## Architecture +- **state.js**: App state definition and helpers +- **update.js**: Pure update function (handles actions/messages) +- **view.js**: Pure view functions (renders HTML as string) +- **api.js**: API fetch logic +- **app.js**: Entrypoint, main loop, event delegation + +## Pattern +- **State**: Single immutable state object +- **Update**: Pure function `(state, action) => newState` +- **View**: Pure function `(state) => html` +- **Entrypoint**: Handles events, dispatches actions, triggers re-render + +## Why? +- Simple, testable, and maintainable +- No dependencies +- Encourages functional, declarative code + +## How to Extend and Use This Template + +This template is designed to be a flexible, opinionated starting point for any browser-native app. + +### Key Files to Extend +- **src/state.js**: Define the app's state shape and any helper functions for cloning or initializing state. +- **src/update.js**: Add new action/message types and update logic. This is where you handle all state transitions. +- **src/view.js**: Build your UI as a pure function of state. Add new components or views here. +- **src/api.js**: Add or replace API calls as needed for your app's data fetching. +- **src/app.js**: Wire up events, use the generalized `render` function, and add any app-specific logic (e.g., focus management, custom event handling). + +### Using the Generalized `render` Function +The `render` function in `app.js` is designed to be reusable for any app. It takes a config object: + +```js +render({ + root, // DOM element to render into + state, // Current app state + view, // View function: (state) => html + events: [ // Array of event bindings + { selector, event, handler }, + // ... + ], + postRender // Optional: function({ root, state }) for custom logic (e.g., focus) +}); +``` + +#### Example: Adding a New Feature +Suppose you want to add a button that increments a counter: + +1. **state.js**: Add `count` to your state. +2. **update.js**: Handle an `INCREMENT` action. +3. **view.js**: Add a button and display the count. +4. **app.js**: + - Add an event handler for the button: + ```js + function handleIncrement() { + dispatch({ type: 'INCREMENT' }); + } + ``` + - Add to the `events` array: + ```js + events: [ + { selector: '#increment-btn', event: 'click', handler: handleIncrement }, + // ...other events + ] + ``` + +### Tips +- Keep all state transitions in `update.js` for predictability. +- Keep all DOM rendering in `view.js` for clarity. +- Use the `postRender` hook for accessibility or focus management. +- Add new features by extending state, update, view, and wiring up events in `app.js`. + +--- + +Inspired by the [Elm Architecture](https://guide.elm-lang.org/architecture/), but using only browser APIs and ES modules. + +--- + +## MIT License + +Copyright 2025 eli_oat + +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. \ No newline at end of file diff --git a/js/seed/index.html b/js/seed/index.html new file mode 100644 index 0000000..08bdd0e --- /dev/null +++ b/js/seed/index.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Starter Kit</title> + <link rel="stylesheet" href="style.css"> +</head> +<body> + <main> + <div id="app"></div> + </main> + <script type="module" src="src/app.js"></script> +</body> +</html> \ No newline at end of file diff --git a/js/seed/src/api.js b/js/seed/src/api.js new file mode 100644 index 0000000..d50c644 --- /dev/null +++ b/js/seed/src/api.js @@ -0,0 +1,15 @@ +// api.js +// API fetch logic + +/** + * Fetch a Pokémon by name from the PokéAPI + * @param {string} name + * @returns {Promise<object>} Pokémon data + */ +export async function fetchPokemon(name) { + const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${encodeURIComponent(name.toLowerCase())}`); + if (!res.ok) { + throw new Error('Pokémon not found'); + } + return await res.json(); +} \ No newline at end of file diff --git a/js/seed/src/app.js b/js/seed/src/app.js new file mode 100644 index 0000000..34b4579 --- /dev/null +++ b/js/seed/src/app.js @@ -0,0 +1,165 @@ +// app.js +// Entrypoint for the app + +import { initialState, cloneState } from './state.js'; +import { update } from './update.js'; +import { view } from './view.js'; +import { fetchPokemon } from './api.js'; +import { initDevMode } from './dev.js'; + +const root = document.getElementById('app'); +let state = cloneState(initialState); +let dev; + +/** + * Entrypoint for the app. + * + * This file implements a minimal Elm-style architecture using only browser APIs and ES modules. + * - All state is immutable and updated by a pure update function. + * - The entire UI is re-rendered as a string on each state change for simplicity and predictability. + * - Event delegation is used to keep wiring minimal and functional. + * - No 3rd party code: everything is browser-native for cozy portability and clarity. + * + * Why this approach? + * - Functional, pure update/view logic is easier for me to reason about and test. + * - Re-rendering the whole UI avoids bugs from manual DOM updates and keeps state/UI in sync. + * - Minimal code and clear data flow make it easy to extend or adapt for new projects. + */ + +// Enable devMode if ?dev=1 is in the URL +/** + * devMode enables logging of all actions and state transitions for debugging. + * + * Why? This makes the app's state flow transparent, helping you understand and debug the app without extra tooling. + */ +const devMode = window.location.search.includes('dev=1'); + +/** + * Generalized render function for Elm-style apps. + * + * @param {Object} config - Render configuration + * @param {HTMLElement} config.root - Root DOM element + * @param {any} config.state - Current app state + * @param {Function} config.view - View function (state => HTML string) + * @param {Array} [config.events] - Array of { selector, event, handler } + * @param {Function} [config.postRender] - Optional function({ root, state }) for post-render logic + */ +function render({ root, state, view, events = [], postRender }) { + root.innerHTML = view(state); + events.forEach(({ selector, event, handler }) => { + const el = root.querySelector(selector); + if (el) el.addEventListener(event, handler); + }); + if (typeof postRender === 'function') { + postRender({ root, state }); + } +} + +// --- App-specific config for render --- +function postRender({ root, state }) { + const input = root.querySelector('#pokemon-query'); + const error = root.querySelector('.error'); + if (error) { + error.focus(); + } else if (input) { + input.focus(); + input.value = state.query; + input.setSelectionRange(input.value.length, input.value.length); + } +} + +function doRender() { + render({ + root, + state, + view, + events: [ + { selector: '#search-form', event: 'submit', handler: handleSubmit }, + { selector: '#pokemon-query', event: 'input', handler: handleInput }, + ], + postRender, + }); +} + +/** + * Dispatches an action to update state and re-render. + * + * Why centralize dispatch? This enforces a single source of truth for state changes, making the app predictable and easy to debug. + * + * Why log actions/state in devMode? This provides a transparent, time-travel-like view of app logic without needing any extra tooling. + */ +function dispatch(action) { + const prevState = state; + state = update(state, action); + if (devMode) { + dev.pushState(state); + console.groupCollapsed(`Action: ${action.type}`); + console.log('Payload:', action.payload); + console.log('Prev state:', prevState); + console.log('Next state:', state); + console.groupEnd(); + } + doRender(); +} + +/** + * Handles input events by dispatching an update action. + * + * Why not update state directly? All state changes go through dispatch/update for consistency and traceability. + */ +function handleInput(e) { + dispatch({ type: 'UPDATE_QUERY', payload: e.target.value }); +} + +/** + * Handles form submission, triggers async fetch, and dispatches state updates. + * + * Why handle async here? Keeps update/view pure and centralizes side-effect. + */ +async function handleSubmit(e) { + e.preventDefault(); + if (!state.query.trim()) return; + dispatch({ type: 'FETCH_START' }); + try { + const data = await fetchPokemon(state.query.trim()); + dispatch({ type: 'FETCH_SUCCESS', payload: data }); + } catch (err) { + dispatch({ type: 'FETCH_ERROR', payload: err.message }); + } +} + +// Initial render +doRender(); + +// After devMode is set +if (devMode) { + dev = initDevMode({ + getState: () => state, + setState: s => { state = s; }, + render: doRender + }); +} + +function updateHistoryInfo() { + if (!devMode || !dev) return; + dev.update(); +} + +function setHistoryPointer(idx) { + const info = dev.getHistoryInfo(); + if (idx < 1 || idx > info.length) return; + const newState = dev.setPointer(idx - 1); + if (newState) { + state = newState; + doRender(); + updateHistoryInfo(); + } +} + +function handleSliderChange(e) { + setHistoryPointer(Number(e.target.value)); +} + +function handleStepperChange(e) { + setHistoryPointer(Number(e.target.value)); +} \ No newline at end of file diff --git a/js/seed/src/dev.js b/js/seed/src/dev.js new file mode 100644 index 0000000..ee1a6e7 --- /dev/null +++ b/js/seed/src/dev.js @@ -0,0 +1,74 @@ +// devMode.js +// Minimal, single-file dev mode with scriptable console API + +/** + * Initialize dev mode: exposes a scriptable API for stepping through state history. + * @param {object} opts + * @param {function} opts.getState - returns current app state + * @param {function} opts.setState - sets app state + * @param {function} opts.render - triggers app re-render + */ +export function initDevMode({ getState, setState, render }) { + let history = []; + let pointer = -1; + let firstLoad = true; + + function pushState(state) { + if (pointer < history.length - 1) history = history.slice(0, pointer + 1); + history.push(clone(state)); + pointer = history.length - 1; + logInstructions(); + } + function goTo(idx) { + if (idx < 0 || idx >= history.length) return; + pointer = idx; + setState(clone(history[pointer])); + render(); + logInstructions(); + } + function next() { + if (pointer < history.length - 1) goTo(pointer + 1); + } + function prev() { + if (pointer > 0) goTo(pointer - 1); + } + function get() { + return history[pointer]; + } + function clone(obj) { + return JSON.parse(JSON.stringify(obj)); + } + function table(obj) { + console.table(dev.history); + } + function logInstructions() { + if (firstLoad) { + console.log('[DevMode] State history debugger'); + console.log('Usage:'); + console.log('- dev.next() // step forward'); + console.log('- dev.prev() // step backward'); + console.log('- dev.goTo(n) // jump to state n (1-based)'); + console.log('- dev.get() // get current state'); + console.log('- dev.table() // display history as a table'); + console.log('- dev.history // array of all states'); + console.log('- dev.pointer // current pointer (0-based)'); + firstLoad = false; + } + } + + // Expose API globally for console use + window.dev = { + next, + prev, + goTo, + get, + table, + get pointer() { return pointer; }, + get history() { return history.slice(); }, + }; + + // Initial state + pushState(getState()); + + return { pushState }; +} \ No newline at end of file diff --git a/js/seed/src/state.js b/js/seed/src/state.js new file mode 100644 index 0000000..d1bad17 --- /dev/null +++ b/js/seed/src/state.js @@ -0,0 +1,13 @@ +// state.js +// App state definition and helpers + +export const initialState = { + query: '', + pokemon: null, + loading: false, + error: null +}; + +export function cloneState(state) { + return JSON.parse(JSON.stringify(state)); +} \ No newline at end of file diff --git a/js/seed/src/update.js b/js/seed/src/update.js new file mode 100644 index 0000000..2f6c13b --- /dev/null +++ b/js/seed/src/update.js @@ -0,0 +1,22 @@ +// update.js +// Pure update function + +/** + * @param {object} state - Current state + * @param {object} action - { type, payload } + * @returns {object} new state + */ +export function update(state, action) { + switch (action.type) { + case 'UPDATE_QUERY': + return { ...state, query: action.payload, error: null }; + case 'FETCH_START': + return { ...state, loading: true, error: null, pokemon: null }; + case 'FETCH_SUCCESS': + return { ...state, loading: false, error: null, pokemon: action.payload }; + case 'FETCH_ERROR': + return { ...state, loading: false, error: action.payload, pokemon: null }; + default: + return state; + } +} \ No newline at end of file diff --git a/js/seed/src/view.js b/js/seed/src/view.js new file mode 100644 index 0000000..5feef6e --- /dev/null +++ b/js/seed/src/view.js @@ -0,0 +1,62 @@ +// view.js +// Pure view functions + +/** + * Pure view functions for the application. + * + * Why pure functions returning HTML strings? + * - Keeps rendering logic stateless and easy to test. + * - Ensures the UI is always a direct function of state, avoiding UI bugs from incremental DOM updates. + * - Using template literals is minimal and browser-native, with no dependencies. + * + * Why escape output? + * - Prevents XSS and ensures all user/content data is safely rendered. + * + * Why semantic/accessible HTML? + * - Ensures the app is usable for all users, including those using assistive tech, and is easy to reason about. + */ +/** + * Render the app UI as an HTML string + * @param {object} state + * @returns {string} + */ +export function view(state) { + return ` + <h1>PokéDex</h1> + <container> + <form id="search-form" autocomplete="off"> + <label for="pokemon-query">Pokémon Name (or number)</label> + <input id="pokemon-query" type="text" value="${escape(state.query)}" placeholder="e.g. pikachu" aria-label="Pokémon Name" required /> + <button type="submit" ${state.loading ? 'disabled' : ''}>${state.loading ? 'Loading...' : 'Search'}</button> + </form> + ${state.error ? `<div class="error" role="alert" tabindex="-1">${escape(state.error)}</div>` : ''} + ${state.pokemon ? renderResult(state.pokemon) : ''} + </container> + `; +} + +function renderResult(pokemon) { + return ` + <div class="result"> + <h2>${capitalize(pokemon.name)} (#${pokemon.id})</h2> + <img class="pokemon-sprite" src="${pokemon.sprites.front_default}" alt="${escape(pokemon.name)} sprite" /> + <ul> + <li>Type: <b>${pokemon.types.map(t => capitalize(escape(t.type.name))).join(', ')}</b></li> + <li>Height: <b>${pokemon.height / 10} m</b></li> + <li>Weight: <b>${pokemon.weight / 10} kg</b></li> + </ul> + </div> + `; +} + +function escape(str) { + /** + * Escapes HTML special characters to prevent XSS. + * + * Why escape here? Keeps all rendering safe by default, so no accidental injection is possible. + */ + return String(str).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); +} +function capitalize(str) { + return str.charAt(0).toUpperCase() + str.slice(1); +} \ No newline at end of file diff --git a/js/seed/style.css b/js/seed/style.css new file mode 100644 index 0000000..da35a7a --- /dev/null +++ b/js/seed/style.css @@ -0,0 +1,82 @@ +body { + --color-bg: #f8f8ff; + --color-text: #222; + --color-main-bg: #fff; + --color-main-border: #222; + --color-shadow: #0001; + --color-label: #222; + --color-input-border: #222; + --color-button-bg: #222; + --color-button-text: #fff; + --color-button-disabled-bg: #888; + --color-result-border: #aaa; + --color-result-bg: #f6f6fa; + --color-error: #b30000; + + font-family: system-ui, sans-serif; + background: var(--color-bg); + color: var(--color-text); + margin: 0; + padding: 0; +} +main { + max-width: 400px; + margin: 3rem auto; + background: var(--color-main-bg); + border: 2px solid var(--color-main-border); + border-radius: 8px; + padding: 2rem 1.5rem; + box-shadow: 0 2px 8px var(--color-shadow); +} +label { + font-weight: bold; + text-transform: uppercase; + font-size: 0.98em; + margin-bottom: 0.2em; + display: block; + color: var(--color-label); +} +input[type="text"] { + width: 100%; + padding: 0.6em; + font-size: 1em; + border: 2px solid var(--color-input-border); + border-radius: 0.2em; + margin-bottom: 1em; + box-sizing: border-box; +} +button { + background: var(--color-button-bg); + color: var(--color-button-text); + border: none; + border-radius: 0.2em; + padding: 0.6em 1.2em; + font-weight: bold; + text-transform: uppercase; + cursor: pointer; + font-size: 1em; + margin-bottom: 1em; +} +button:disabled { + background: var(--color-button-disabled-bg); + cursor: not-allowed; +} +.result { + margin-top: 1.5em; + padding: 1em; + border: 1.5px solid var(--color-result-border); + border-radius: 0.3em; + background: var(--color-result-bg); +} +.pokemon-sprite { + width: 120px; + height: 120px; + object-fit: contain; + margin: 0 auto; + display: block; +} +.error { + color: var(--color-error); + font-weight: bold; + margin-top: 1em; +} \ No newline at end of file diff --git a/perl/functional/main.pl b/perl/functional/main.pl new file mode 100755 index 0000000..2198cf3 --- /dev/null +++ b/perl/functional/main.pl @@ -0,0 +1,227 @@ +#!/usr/bin/env perl + +# ----------------------------------------------------------------------------- +# +# main.pl - A Functional Programming Starting Point for Perl Projects +# +# ----------------------------------------------------------------------------- +# This script serves as a template for new Perl projects, emphasizing a +# functional, pragmatic style. It includes standard setup, custom functional +# utilities, and an architectural pattern for separating pure logic from + +# impure I/O, inspired by The Elm Architecture. +# ----------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------- +# SECTION 1: STANDARD SETUP (PRAGMAS) +# ----------------------------------------------------------------------------- +# These pragmas enforce modern, safer Perl conventions. +# +# - strict: Enforces rules to prevent common mistakes, like typos in variable +# names. It requires you to declare variables with `my`. +# - warnings: Catches potential problems and questionable coding practices, +# printing warnings to STDERR. +# - feature: Enables modern language features. 'say' is a newline-aware +# print. 'signatures' provides clean subroutine argument lists. +# ----------------------------------------------------------------------------- +use strict; +use warnings; +use feature qw(say signatures); + +# List::Util is a core module (no extra install needed). It provides highly +# optimized list-processing functions. We use `reduce` (also known as `foldl`) +# as a fundamental building block for other functions. +use List::Util 'reduce'; + +# For demonstrating immutability on complex data structures. +# In a real project, you would `cpanm Readonly` to install this. +# For this example, we will just use convention. +# use Readonly; # Uncomment if you have the Readonly module installed. + + +# ----------------------------------------------------------------------------- +# SECTION 2: FUNCTIONAL UTILITIES +# ----------------------------------------------------------------------------- +# These are custom-built, higher-order functions that form the core of a +# functional toolkit. They operate on other functions to create new, more +# powerful functions. +# ----------------------------------------------------------------------------- + +# +# compose(@funcs) +# +# Takes a list of functions and returns a new function that applies them +# right-to-left. +# +# Example: compose($f, $g, $h)->($x) is equivalent to $f($g($h($x))). +# +# This is useful for building complex logic by chaining together simple, +# reusable functions in a readable, mathematical style. +# +sub compose(@funcs) { + return sub ($initial_value) { + return reduce { $a->($b) } $initial_value, reverse @funcs; + }; +} + +# +# pipe(@funcs) +# +# Takes a list of functions and returns a new function that applies them +# left-to-right. This is often more intuitive than compose for data +# processing workflows. +# +# Example: pipe($f, $g, $h)->($x) is equivalent to $h($g($f($x))). +# +# This reads like a sequence of steps, e.g., "take x, then do f, then do g". +# +sub pipe(@funcs) { + return sub ($initial_value) { + return reduce { $b->($a) } $initial_value, @funcs; + }; +} + +# +# curry($func, @args) +# +# Transforms a function that takes multiple arguments into a function that +# takes some of those arguments now, and the rest later. +# +# Example: +# my $add = sub ($x, $y) { $x + $y }; +# my $add_five = curry($add, 5); # Creates a new function that adds 5. +# my $result = $add_five->(10); # Result is 15. +# +# This is excellent for creating specialized versions of general functions. +# +sub curry($func, @args) { + return sub { + return $func->(@args, @_); + }; +} + +# ----------------------------------------------------------------------------- +# SECTION 3: ARCHITECTURE - PURE LOGIC VS IMPURE I/O +# ----------------------------------------------------------------------------- +# This section demonstrates a pattern for structuring applications to achieve +# a clean separation of concerns. All core logic is contained in a single, +# pure `update` function. All I/O (printing, reading, etc.) is handled +# outside of it. This makes the core logic extremely easy to test and reason +# about. +# +# This pattern is a simplified version of The Elm Architecture. +# - Model: The state of your application. +# - Msg: A description of something that happened (e.g., user input). +# - update: A pure function that computes the new state. +# ----------------------------------------------------------------------------- + +# +# update($msg, $model) +# +# This is the PURE core of the application. +# It takes a message (describing an event) and the current state (the model), +# and it returns the NEW state. +# +# Crucially, this function does NO I/O. It doesn't print, read files, or have +# any other "side effects". Its only job is to compute the next state. +# Because it's pure, you can call it with the same arguments 100 times and +# always get the same result, making it perfectly predictable and testable. +# +sub update($msg, $model) { + # Create a mutable copy to work with. The original $model is not changed. + my %new_model = %{$model}; + + if ($msg->{type} eq 'INCREMENT') { + $new_model{counter} += 1; + } + elsif ($msg->{type} eq 'ADD_X') { + $new_model{counter} += $msg->{payload}; + } + elsif ($msg->{type} eq 'SET_NAME') { + $new_model{name} = $msg->{payload}; + } + + # Return a reference to the new model. + # Note: A more robust implementation would ensure a deep, immutable copy. + # For many scripts, convention is sufficient. + return \%new_model; +} + + +# ----------------------------------------------------------------------------- +# SECTION 4: MAIN EXECUTION BLOCK (The "IMPURE" Runtime) +# ----------------------------------------------------------------------------- +# This is the imperative shell that orchestrates the application. It handles +# all I/O and calls the pure `update` function to process state changes. +# ----------------------------------------------------------------------------- +sub main() { + say "--- Functional Perl Starter ---"; + + # --- Part 1: Demonstrating Built-in Functional Operators --- + say "\n1. Demonstrating map and reduce (from List::Util)"; + my @numbers = (1 .. 5); + + # `map` applies a function to each element of a list, returning a new list. + my @doubled = map { $_ * 2 } @numbers; + say "Original: @numbers"; + say "Doubled: @doubled"; + + # `reduce` combines all elements of a list into a single value. + my $sum = reduce { $a + $b } 0, @numbers; # Start with an accumulator of 0 + say "Sum: $sum"; + + + # --- Part 2: Demonstrating Custom Functional Utilities --- + say "\n2. Demonstrating pipe, compose, and curry"; + + my $add_one = sub ($n) { $n + 1 }; + my $mult_two = sub ($n) { $n * 2 }; + my $sub_three = sub ($n) { $n - 3 }; + + # pipe: (5 + 1) * 2 - 3 = 9 + my $pipeline_op = pipe($add_one, $mult_two, $sub_three); + say "pipe(5): " . $pipeline_op->(5); + + # compose: 5 * 2 + 1 - 3 = 8 + my $compose_op = compose($sub_three, $add_one, $mult_two); + say "compose(5): " . $compose_op->(5); + + # curry + my $add = sub ($x, $y) { $x + $y }; + my $add_ten = curry($add, 10); + say "curry(add 10 to 7): " . $add_ten->(7); # 17 + + + # --- Part 3: Demonstrating the Pure/Impure Architecture --- + say "\n3. Demonstrating The Elm-style Architecture"; + + # The initial state of our application. + my $model = { + counter => 0, + name => "World", + }; + + say "Initial Model: counter = $model->{counter}, name = $model->{name}"; + + # We simulate a sequence of events (messages). + my @events = ( + { type => 'INCREMENT' }, + { type => 'INCREMENT' }, + { type => 'SET_NAME', payload => 'Perl' }, + { type => 'ADD_X', payload => 10 }, + ); + + # The "Runtime Loop": Process each event through the pure `update` function. + # The runtime's job is to call `update` and then perform I/O (like `say`). + for my $msg (@events) { + say " -> Processing event: $msg->{type}"; + $model = update($msg, $model); # Calculate new state + } + + say "Final Model: counter = $model->{counter}, name = $model->{name}"; + + return 0; +} + +# Run the main subroutine. +exit main(); diff --git a/ts/thinking-about-unions/left-pad.ts b/ts/thinking-about-unions/left-pad.ts index e75e38d..95f6d51 100644 --- a/ts/thinking-about-unions/left-pad.ts +++ b/ts/thinking-about-unions/left-pad.ts @@ -1,9 +1,9 @@ -/* +/* A stupidly simple example of unions. Unions can be used to describe a type that is actually several different types. -Here, the Padding type is a union of either a number or a string. +Here, the Padding type is a union of either a number or a string. Then, leftPad uses the union type so that it can accept either sort of type. */ @@ -11,16 +11,17 @@ Then, leftPad uses the union type so that it can accept either sort of type. type Padding = number | string; const leftPad = (value: string, padding: Padding) => { - if (typeof padding === 'number') { - return Array(padding + 1).join(' ') + value; // 0 indexing is for computers, this function is for people. - } - if (typeof padding === 'string') { - return padding + value; - } - throw new Error(`Expected number or string, got '${padding}'.`); -} + switch (typeof padding) { + case "number": + return Array(padding + 1).join(" ") + value; // 0 indexing is for computers, this function is for people. + case "string": + return padding + value; + default: + throw new Error(`Expected number or string, got '${padding}'.`); + } +}; -const marioMsg = 'It is I, Mario!'; +const marioMsg = "It is I, Mario!"; console.log(leftPad(marioMsg, 4)); -console.log(leftPad(marioMsg, "****")); -console.log(leftPad(marioMsg, true)); \ No newline at end of file +console.log(leftPad(marioMsg, "*** ")); +// console.log(leftPad(marioMsg, true)); |