'use strict' function Commander (client) { this.el = document.createElement('div') this.el.id = 'commander' this._input = document.createElement('textarea') this._status = document.createElement('div'); this._status.id = 'status' this._log = document.createElement('div'); this._log.id = 'log' this._docs = document.createElement('div'); this._docs.id = 'help' this._eval = document.createElement('a'); this._eval.id = 'eval' this.isVisible = true this.install = function (host) { this.el.appendChild(this._input) this._status.appendChild(this._log) this._status.appendChild(this._docs) this._status.appendChild(this._eval) this.el.appendChild(this._status) host.appendChild(this.el) this._eval.setAttribute('title', 'Eval(c-R)') this._input.setAttribute('autocomplete', 'off') this._input.setAttribute('autocorrect', 'off') this._input.setAttribute('autocapitalize', 'off') this._input.setAttribute('spellcheck', 'false') this._input.addEventListener('input', this.onInput) this._input.addEventListener('click', this.onClick) this._eval.addEventListener('click', () => { this.eval() }) this._input.onkeydown = (e) => { if (e.keyCode === 9 || e.which === 9) { e.preventDefault(); this.inject(' ') } } client.surface.maximize() } this.start = function () { this.show() this._input.value = this.splash setTimeout(() => { this.eval() }, 1000) this.setStatus('Ready.') } this.eval = (txt = this._input.value) => { if (this._input.value.indexOf('$') > -1) { txt = this.clean(txt) } client.bindings = {} client.lain.run(`(${txt})`) this.feedback() } this.evalSelection = () => { const value = this._input.value.substr(this._input.selectionStart, this._input.selectionEnd) client.lain.run(`(${value})`) this.feedback() } this.load = function (txt) { this._input.value = txt this.eval(txt) } this.clear = function () { this.load('') } this.cleanup = function () { this._input.value = this.clean(this._input.value) this.lint() this.eval() } this.update = function () { } this.onInput = () => { this.setStatus() } this.onClick = () => { this.setStatus() } this.clean = function (input) { const keywords = ['$pos+', '$pos', '$rect', '$line', '$x', '$y', '$xy'] for (const word of keywords) { input = input.replace(word, '').trim() } return input } this.setStatus = function (msg) { // Logs if (msg && msg !== this._log.textContent) { this._log.textContent = `${msg}` } this._docs.textContent = this.getDocs() } // Injection this.cache = this._input.value this.capture = function () { if (this._input.value.indexOf('$') < 0) { return } this.cache = this._input.value } this.inject = function (injection, at = this._input.selectionStart) { this._input.value = this._input.value.substring(0, this._input.selectionStart) + injection + this._input.value.substring(this._input.selectionEnd) this._input.selectionEnd = at + injection.length } this.injectPath = function (path) { if (this._input.value.indexOf('$') < 0) { return } this._input.value = this._input.value.replace('$path', `"${path}"`) } // Helpers this.commit = function (shape, end = false, run = false) { if (this.cache.indexOf('$') < 0) { return } const segs = this.cache.split('$') const words = segs[1].split(' ') const word = words[0].split(/[^A-Za-z]/)[0] const append = words[0].indexOf('+') > -1 if (word === 'drag') { this.cache = this.cache.replace('$drag', '(drag $rect $line)') } else if (word === 'view') { this.cache = this.cache.replace('$view', '(view $rect $rect)') } else if (word === 'poly') { this.cache = this.cache.replace('$poly', '(poly $pos+)') } else if (word === 'move') { this.cache = this.cache.replace('$move', '(transform:move $wh)') } else if (word === 'rotate') { this.cache = this.cache.replace('$rotate', '(transform:rotate $a)') } if (shape[word]) { if (append) { this._input.value = this.cache.replace('$' + word + '+', this.template(shape[word], word) + ' $' + word + '+') } else { this._input.value = this.cache.replace('$' + word, this.template(shape[word], word)) } } if (end === true) { this.cache = this._input.value } if (run === true) { this.eval() } } this.template = function (shape, word) { if (word === 'rect') { return `(rect ${shape.x} ${shape.y} ${shape.w} ${shape.h})` } if (word === 'pos') { return `(pos ${shape.x} ${shape.y})` } if (word === 'line') { return `(line ${shape.a.x} ${shape.a.y} ${shape.b.x} ${shape.b.y})` } if (word === 'circle') { return `(circle ${shape.cx} ${shape.cy} ${shape.r})` } if (word === 'arc') { return `(arc ${shape.cx} ${shape.cy} ${shape.r} ${shape.sa} ${shape.ea})` } if (word === 'x' || word === 'y' || word === 'xy' || word === 'wh' || word === 'a' || word === 'r') { return `${shape}` } return '' } // Display this.show = (expand = false) => { if (this.isVisible === true && expand !== true) { return } client.el.className = expand ? 'expand' : '' this.isVisible = true this._input.focus() } this.hide = () => { if (this.isVisible !== true) { return } client.el.className = 'hidden' this.isVisible = false this._input.blur() } this.toggle = (expand = false) => { if (this.isVisible !== true) { this.show(expand) } else { this.hide() } } this.length = function () { return this._input.value.split('\n').length } this.feedback = function () { this._eval.className = 'active' setTimeout(() => { this._eval.className = '' }, 150) } // Docs this.getCurrentWord = () => { const pos = this._input.value.substr(0, this._input.selectionStart).lastIndexOf('(') return this._input.value.substr(pos).split(' ')[0].replace(/\(/g, '').replace(/\)/g, '').trim() } this.getCurrentFunction = () => { const word = this.getCurrentWord() let mostSimilar = '' if (client.library[word]) { return word } for (const id of Object.keys(client.library)) { if (id.substr(0, word.length) === word) { mostSimilar = id } } return mostSimilar } this.getDocs = (id) => { const name = this.getCurrentFunction() const fn = client.library[name] if (!fn) { return } const fnString = fn.toString() if (fnString.indexOf(') => {') < 0) { return } const fnParams = fnString.split(') => {')[0].substr(1).split(',').reduce((acc, item) => { return `${acc}${item.indexOf('=') > -1 ? '~' + item.split('=')[0].trim() : item} ` }, '').trim() return `(${(name + ' ' + fnParams).trim()})` } this.lint = function () { const value = this._input.value if (value.split('(').length !== value.split(')').length) { return client.log('Uneven number of parens.') } this._input.value = lintLISP(value) } // Splash this.splash = `; Ronin v2.50 (def logo-path "M60,60 L195,60 A45,45 0 0,1 240,105 A45,45 0 0,1 195,150 L60,150 M195,150 A45,45 0 0,1 240,195 L240,240 ") (clear) (resize 600 600) (stroke (svg 140 140 logo-path) "black" 7)` } function lintLISP (str) { // cleanup let val = str.replace(/\n/g, '').replace(/ \)/g, ')').replace(/ +(?= )/g, '').replace(/\( \(/g, '((').replace(/\) \)/g, '))').trim() // begin let depth = 0 for (let i = 0; i < val.length; i++) { const c = val.charAt(i) depth += c === '(' ? 1 : c === ')' ? -1 : 0 // Pad comments if (c === ';') { const indent = '\n' + (' '.repeat(depth)) val = [val.slice(0, i), `${indent}`, val.slice(i)].join('') i += indent.length } // Don't pad when closing on next char if (c === '(' && val.charAt(i + 1) !== ')') { const indent = '\n' + (' '.repeat(depth - 1)) val = [val.slice(0, i), `${indent}`, val.slice(i)].join('') i += indent.length } // Add linebreak after paren at depth 0 if (c === ')' && depth === 0) { val = [val.slice(0, i), ')\n', val.slice(i + 1)].join('') } } // Space out comments val = val.split('\n').map((line) => { return line.substr(0, 2) === '; ' ? `\n${line}\n` : line }).join('\n') return val.trim() }