about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--html/playground/APL386.ttfbin0 -> 203668 bytes
-rw-r--r--html/playground/index.html243
-rw-r--r--html/playground/scheme.html533
-rw-r--r--html/schemer/index.html305
-rw-r--r--html/schemer/tls.pdfbin0 -> 2359953 bytes
-rw-r--r--html/squine.html1108
-rw-r--r--uiua/life.ua7
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