diff options
-rw-r--r-- | html/playground/APL386.ttf | bin | 0 -> 203668 bytes | |||
-rw-r--r-- | html/playground/index.html | 243 | ||||
-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-- | uiua/life.ua | 7 |
7 files changed, 2193 insertions, 3 deletions
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/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/uiua/life.ua b/uiua/life.ua index 7027301..706c2da 100644 --- a/uiua/life.ua +++ b/uiua/life.ua @@ -1,5 +1,6 @@ # lifted from the uiua docs Life ← ↥⊙↧∩=3,2-,/+≡↻☇1-1⇡3_3¤. -⁅×0.6∵⋅⚂↯⊟.30 0 # Init -⇌;⍥(⊃∘⊂Life)100⊃∘(↯1) # Run -≡(▽↯⧻,:⍉▽↯⧻,,:5) # Upscale +⁅×0.6∵⋅⚂↯⊟.30 0 # Init +⍥(⊃∘⊂Life)100⊃∘(↯1) # Run +⇌ +≡(▽↯⧻,:⍉▽↯⧻,,:5) # Upscale |