formula sheet in DHTML (sketch of a Bicicleta feature)
Kragen Javier Sitaker
kragen at pobox.com
Sat Feb 17 03:37:02 EST 2007
A couple of times previously, I've posted some dynamic calculation
hack in Python with Tk, where the calculation results are displayed as
you type, and you can define several variables that depend on one
another's values.
Here's the same thing, in DHTML for Firefox, with a small
improvement --- most of the time, it tracks dependencies
between formulas. It only works in Mozilla (Firefox or Seamonkey)
because it depends on the specific format of the ReferenceError
message and on .toSource().
This will be on the web at
http://pobox.com/~kragen/sw/formulasheet/formulasheet.html for easier
viewing.
<html><head><title>Formula sheet sketch</title>
<script type="text/javascript">//<![CDATA[
function meta_tracing_eval(expr, env) {
var minimal_env = {}
var err = null
var val = null
var exists = {}
var minimal_env_exists = {}
for (;;) {
try {
with(minimal_env) { val = eval(expr) }
} catch (e) {
if (e.constructor == ReferenceError) {
var parsed = e.message.match(/(.*) is not defined/)
var name = parsed[1]
if (minimal_env_exists[name]) { // we already inserted it...
err = e
break
}
if (!env.exists(name)) {
minimal_env_exists[name] = 1
err = e
break
}
minimal_env[name] = env.get(name)
minimal_env_exists[name] = 1
continue
} else {
err = e
break
}
}
break
}
return {vars: minimal_env_exists, error: err, value: val}
}
function tracing_eval(expr, env) {
var exists = {}
for (var name in env) exists[name] = 1
var meta_env = {
exists: function(name) { return exists[name] },
get: function(name) { return env[name] },
}
return meta_tracing_eval(expr, meta_env)
}
function keys(obj) {
var rv = []
for (var name in obj) rv.push(name)
return rv
}
// this needs to not hang:
function hairy_example() {
var env = { wrong: function() { return nonexistent_var } }
tracing_eval('wrong()', env)
}
hairy_example()
// this needs to not give an error:
function falsity_example() {
var env = { cond: 0, a: 3, b: 4 }
var rv = tracing_eval('cond ? a : b', env)
if (rv.error) alert('freevars test error: ' + rv.error)
}
falsity_example()
// there was still a bug with the combination
function combo_example() {
var env = { funnyvar: 0, wrong: function() { return funnyvar } }
tracing_eval('wrong()', env)
}
combo_example()
function easy_example() {
var env = { a: 3, b: 4 }
var result = tracing_eval('a + b', env)
if (result.error) alert('freevars easy test error: ' + result.error)
if (result.value != 7) alert('freevars easy test result: ' + result.value)
var vars = keys(result.vars)
if (!result.vars['a'] || !result.vars['b'] || vars.length != 2)
alert('freevars easy test vars: ' + vars.toSource())
}
easy_example()
function notfound_example() {
var result = tracing_eval('nonexistent', {})
if (!result.vars['nonexistent'])
alert('freevars notfound vars: ' + result.vars.toSource())
}
notfound_example()
function meta_example() {
var meta_len_env = {
exists: function() { return true },
get: function(name) { return name.length },
}
var result = meta_tracing_eval('xxx + yy', meta_len_env)
if (result.value != 5)
alert('freevars meta example: ' + result.toSource())
}
meta_example()
// ]]>
</script>
<script type="text/javascript">// <![CDATA[
/* dynamic expression evaluation environment in the browser */
/* I didn't reinvent the wheel for fun here, exactly; I reinvented it
because I wrote this on a machine without internet access or an
already-downloaded copy of MochiKit. Not using jQuery is less
defensible. */
/* basic wheel reinvention: iteration */
function forEach(array, callback) {
for (var ii = 0; ii < array.length; ii++) callback(array[ii])
}
/* basic wheel reinvention: DOM */
function $(id) {
return document.getElementById(id)
}
function hasClass(node, className) {
if (!node.className) return false
var classes = node.className.split(/\s+/)
for (var ii=0; ii < classes.length; ii++) {
if (classes[ii] == className) return true
}
return false
}
function descendantsByClass(elem, className) {
var rv = []
var stack = [elem]
while (stack.length) {
var node = stack.pop()
if (hasClass(node, className)) rv.push(node)
for (var ii = node.childNodes.length - 1; ii >= 0; ii--) {
stack.push(node.childNodes[ii])
}
}
return rv
}
/* this one function may not be wheel reinvention. It finds the
tree-nearest cousin with a particular class name, so given a formula and
'varname', it can find the varname, or given the varname and
'formula', it can find the corresponding formula. */
function cousin(elem, className) {
while (elem != document) {
elem = elem.parentNode
var rv = descendantsByClass(elem, className)
if (rv.length == 1) return rv[0]
if (rv.length > 1) throw "too many cousins of " + className /* untested */
}
throw "no cousin found: " + className /* error handling untested */
}
function replaceChildren(node, newChild) {
while (node.firstChild) node.removeChild(node.firstChild)
node.appendChild(newChild)
}
function createDOM(tagname, attributes, contents) {
var rv = document.createElement(tagname)
for (var attr in attributes) {
rv.setAttribute(attr, attributes[attr])
}
if (contents) {
forEach(contents, function(item) {
if (item.constructor == String) item = document.createTextNode(item)
rv.appendChild(item) /* XXX handle arrays */
})
}
return rv
}
function createDOMSugar(tagname, args) {
var attributes = args[0]
var contents = []
for (ii = 1; ii < args.length; ii++) contents.push(args[ii])
return createDOM(tagname, attributes, contents)
}
function TR() { return createDOMSugar('TR', arguments) }
function TD() { return createDOMSugar('TD', arguments) }
function INPUT() { return createDOMSugar('INPUT', arguments) }
function B() { return createDOMSugar('B', arguments) }
/* basic wheel reinvention: repr, display a JavaScript value as text */
function toSource(val) {
if (val == null) return 'null'
else if (val.constructor == Number) return val.toString()
else if (val.constructor == String) { /* cheating! */
var rv = [val].toSource() /* now we have leading [ and trailing ] */
return rv.substr(1, rv.length - 2)
}
else return val.toSource()
}
/* end of wheel reinvention; actual program starts here */
function Row(namefield, formulafield, result_loc) {
this.namefield = namefield
this.formulafield = formulafield
this.result_loc = result_loc
this.vars = {}
}
Row.prototype.eval = function(env) {
var textresult
var result = meta_tracing_eval('(' + this.formulafield.value + ')', env)
this.vars = result.vars
if (result.error) {
textresult = result.error.toString()
delete this.result
} else {
this.result = result.value
textresult = toSource(result.value)
}
replaceChildren(this.result_loc, document.createTextNode(textresult))
return result
}
Row.prototype.name = function() { return this.namefield.value }
Row.prototype.depends_on = function(name) { return this.vars[name] }
function Env() {
this.rows = []
this.vars = new VarFacade(this)
}
Env.prototype.updateRow = function(row) {
var result = row.eval(this.vars)
var name = row.name()
if (!result.error && name != '') this.vars[name] = result.value
var self = this
if (!name) return
forEach(this.rows, function(other_row) {
if (other_row.depends_on(name))
setTimeout(function() { self.updateRow(other_row) }, 0)
})
}
Env.prototype.addRow = function(row) {
this.rows.push(row)
this.updateRow(row)
}
function VarFacade(env) { this.env = env }
VarFacade.prototype.exists = function(name) {
return this.getRow(name)
}
VarFacade.prototype.getRow = function(name) {
for (var ii = 0; ii < this.env.rows.length; ii++) {
var row = this.env.rows[ii]
if (row.name() == name) return row
}
return null
}
VarFacade.prototype.get = function(name) {
var row = this.getRow(name)
// the exists() check in meta_tracing_eval should prevent this, but...
if (!row) throw new ReferenceError('meta var ' + name)
return row.result
}
function ancestorOfType(elem, type) {
while (elem.tagName != type) elem = elem.parentNode
return elem
}
function addRowEventHandler(env) {
return function(ev) {
var tr = ancestorOfType(ev.target, 'TR')
var where = { elem: tr.parentNode, pos: tr }
return addRow(env, where)
}
}
function addRow(env, where, formula, name) {
if (!formula) formula = "3 + 4"
if (!name) name = ''
var formula_f = INPUT({'class': 'formula', value: formula})
var name_f = INPUT({'class': 'varname', value: name})
var result_f = TD({'class': 'result'})
where.elem.insertBefore(TR({}, TD({}, name_f, B({}, ':')),
TD({}, formula_f), result_f),
where.pos)
var row = new Row(name_f, formula_f, result_f)
env.addRow(row)
formula_f.addEventListener('keyup', function(ev) { env.updateRow(row) }, true)
name_f.addEventListener('keyup', function(ev) { env.updateRow(row) }, true)
setTimeout(function() { formula_f.focus() }, 0) // "permission denied"?!
}
global_env = null
function init() {
var env = new Env()
global_env = env
var rowadders = descendantsByClass(document, 'addrow')
var tr = ancestorOfType(rowadders[0], 'TR')
var where = { elem: tr.parentNode, pos: tr }
addRow(env, where, '"These are days"', 'text')
addRow(env, where, 'text.split(/\\s+/)', 'words')
addRow(env, where, 'function (f, xs) { var rv = []; for (var ii = 0; ii < xs.length; ii++) rv.push(f(xs[ii])); return rv;}', 'map')
addRow(env, where, 'map(function(x) { return x.toLowerCase() }, words)')
addRow(env, where, 'text.length')
addRow(env, where, 'words.length')
forEach(rowadders, function(el) {
el.addEventListener('click', addRowEventHandler(env), true)
})
}
window.addEventListener('load', init, true)
/* TODO: (baby steps because i am feeling chicken)
D move the + back to the bottom where it belongs!
- don't have endless loops on circular refs
X this is sort of solved now in the sense that it no longer hangs the
browser, but it does use absurd amounts of CPU time
D delete obsolete name bindings when names get edited
D propagate values and update bindings when names change (not just when
formulas change)
- no, really, propagate values to dependents when name goes away
D propagate errors to dependents (right now old values stick around)
- don't update dependents when value didn't change
D delete obsolete dependencies
- is there a way to clean up the evaluation environment so things like
"document" and "window" disappear? And especially things like "env"!
- maybe use getter= instead of parsing ReferenceError?
- maybe pack into a bookmarklet :)
- adjust input width to fit text (is this even possible?)
- make it not look like shit:
- align text to top
- errors in red, with an error icon
- other values need better-laid-out representations (this is HTML, not ASR-33)
- different columns in different colors?
- nicer icon for "+"
- enhance user control:
- make it possible to rearrange row order
- make it possible to delete old rows
- continue displaying old values after an error; only show error on request
- Do Something Cool, like:
- a picture data type
- numerical inputs with sliders
- current mouse position
- delayed mouse position. etc.
- handle events other than keyup
*/
//]]>
</script>
<style type="text/css">
.formula, .varname { border: 0; font: inherit }
.formula:hover, .varname:hover { background-color: #ffd }
.varname { text-align: right }
/* does not work: .varname:after { content: ":" } */
</style>
</head><body>
<h1>Formula sheet sketch</h1>
<p>This is a very limited DHTML mockup of a Bicicleta feature. Here
we have some names and some corresponding definitions, and we can see
the current values of the names as computed from the definitions. If
you alter a definition, all the values depending on it change
correspondingly, in real time.</p>
<p>There are a couple of "features" in it that are intended to be
removed in the real Bicicleta:</p>
<ul>
<li>There's no distinction between things that have side effects and
things that don't — it's easy to accidentally create something
that has unwanted side effects.</li>
<li>It has some difficulty telling what's inside a function that has
free variables. If you define a function as "function(x) { return
otherfunc(x) + 1}", it won't notice the "otherfunc" until it's too
late. You can work around this by defining it as "otherfunc,
function(x) { return otherfunc(x) + 1}".</li>
<li>It's possible to construct a self-referential definition which
will just keep on updating endlessly, although it takes a little
work.</li>
<li>It's straightforward to create a program that takes exponential
time and has "glitches" where it temporarily has the wrong answer;
define a: 1, a1: a, b: a+a1, b1: b, c: b+b1, c1: c, and so on
through k, and the convergent data flow causes several seconds of
slowness whenever you have a keyup event in the a field.</li>
<li>You can only define functions in JavaScript, not in the
environment itself.</li>
</ul>
<table id="where">
<tr><th align="right">name:</th><th>formula</th><th>value</th></tr>
<tr><td class="addrow">+</td></tr>
</table>
</body></html>
More information about the Kragen-hacks
mailing list