diff options
-rw-r--r-- | html/squine.html | 1108 |
1 files changed, 1108 insertions, 0 deletions
diff --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 |