This is on the Web at http://pobox.com/~kragen/sw/dhtml-sniki.html. I
wrote it in May but for some reason, as far as I can tell, I never got
around to telling anyone about it, perhaps because I was too ashamed of
the lameness of the implementation.
Like everything else posted to this mailing list without notices to the
contrary, this is in the public domain.
<html>
<head>
<title>Sniki-like DHTML</title>
<!--
Darius Bacon wrote this amazingly cool hack called SnikiSniki, a
Wiki that contained typed links --- rather like RDF triples. I
wanted to play with this same idea in DHTML (probably with Ajax
persistence eventually).
This implementation is really lame in several ways:
- My implementation of depth-first search on the "knowledge base" is
awfully clumsy; I anticipate that in time I will learn to express
this concept more clearly in code.
- Despite living in the browser, the user interface is the
traditional "type text strings into a small box and hit enter"
that we've all come to know and love over the last few decades.
- There's no way to get the first few results before chewing up
large amounts of browser time.
-->
<script src="http://pobox.com/~kragen/sw/jstest-v1.js"></script>
<script>
// -*- c -*-
// simple (yet over-complex) triple-store library; originally in a
// separate file, included here for email
function makebits() {
var stack = [[]]
var results = []
while (stack.length) {
var next = stack.pop()
if (next.length == 3) results.push(next)
else {
var choices = [1, 0]
for (var choice in choices) {
stack.push(next.concat([choices[choice]]))
}
}
}
return results
}
function split_triple(triple) {
return triple.split(/\s+/)
}
function is_var(atomname) {
return atomname.match(/^[A-Z]/)
}
function freevars(triples) {
var rv = []
var seen = {}
for (var ii in triples) {
var items = triples[ii]
for (var jj in items) {
if (is_var(items[jj]) && !seen[items[jj]]) {
rv.push(items[jj])
seen[items[jj]] = 1
}
}
}
return rv
}
function make_object(names, values) {
var rv = {}
for (var ii in names) rv[names[ii]] = values[ii]
return rv
}
function map(cb, list) {
var rv = []
for (var ii = 0; ii < list.length; ii++) rv.push(cb(list[ii]))
return rv
}
function instantiate(triples, instantiations) {
return map(function(triple) {
return map(function(atom) {return instantiations[atom] || atom}, triple)
}, triples)
}
function queryfunction(query_triples) {
var split_query_triples = map(split_triple, query_triples)
var variables = freevars(split_query_triples)
var stack = [[]]
var rv = []
while (stack.length) {
var assignments = stack.pop()
var state = make_object(variables, assignments)
var instantiated_triples = instantiate(split_query_triples, state)
if (this.may_contain(instantiated_triples)) {
if (assignments.length == variables.length) {
rv.push(state)
} else {
var current_variable = variables[assignments.length]
var possible_values =
this.get_candidate_values(instantiated_triples, current_variable)
for (var ii in possible_values) {
stack.push(assignments.concat([possible_values[ii]]))
}
}
}
}
return rv
}
function triple_match(pattern, triple) {
var rv = {}
for (var jj in pattern)
if (!is_var(pattern[jj]) && pattern[jj] != triple[jj]) return null
else rv[pattern[jj]] = triple[jj]
// XXX note that this will be overly optimistic for multiple
occurrences of the variable!
return rv
}
function contains(array, item) {
for (var ii = 0; ii < array.length; ii++)
if (array[ii] == item) return true
return false
}
function triplestore() {
this.triples = []
this.add = function(triples) {
for (var x in triples) this.triples.push(split_triple(triples[x]))
}
this.query = queryfunction
this.may_contain = function(triples) {
for (var ii in triples)
if (!this.may_contain_triple(triples[ii])) return false
return true
}
this.may_contain_triple = function(triple) {
for (var ii in this.triples) {
if (triple_match(triple, this.triples[ii])) return true;
}
return false
}
this.get_candidate_values = function(triples, variable) {
// XXX highly duplicative of may_contain!
var rv = []
var seen = {}
for (var ii in triples)
if (contains(triples[ii], variable))
for (var jj in this.triples) {
var match = triple_match(triples[ii], this.triples[jj])
if (match && !seen[match[variable]]) {
rv.unshift(match[variable])
seen[match[variable]] = 1
}
}
return rv
}
}
tests = {
bits: function() {
assert_equal(makebits(), [[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1],
[1, 0, 0], [1, 0, 1], [1, 1, 0], [1, 1, 1]])
},
is_var: function() {
assert(is_var("A"), "A")
assert(!is_var("a"), "a")
assert(is_var("Ax"), "Ax")
assert(!is_var("xA"), "xA")
assert(is_var("Z"), "Z")
},
freevars: function() {
assert_equal(freevars([]), [])
assert_equal(freevars([['a', 'b', 'c']]), [])
assert_equal(freevars([['a', 'b', 'C']]), ['C'])
assert_equal(freevars([['A', 'B', 'C']]), ['A', 'B', 'C'])
assert_equal(freevars([['A', 'A', 'A']]), ['A'])
},
make_object: function() {
assert_equal(make_object(['beatrice', 'kragen', 'mikel'], ['murch',
'sitaker', 'jay']),
{beatrice: 'murch', kragen: 'sitaker', mikel: 'jay'})
},
triplestore: function() {
var ts = new triplestore()
ts.add(['a b c'])
var yes = [{}]
var no = []
assert_equal(ts.query([]), yes)
assert_equal(ts.query(['a b c']), yes)
assert_equal(ts.query(['a b d']), no)
assert_equal(ts.query(['a b c', 'a b c']), yes)
assert_equal(ts.query(['a b c', 'a b d']), no)
assert_equal(ts.query(['a b d', 'a b d']), no)
assert_equal(ts.query(['a b d', 'a b c']), no)
assert_equal(ts.query(['a b X']), [{X: 'c'}])
assert_equal(ts.query(['a X c']), [{X: 'b'}])
assert_equal(ts.query(['X b c']), [{X: 'a'}])
assert_equal(ts.query(['a X d']), no)
assert_equal(ts.query(['a X Y']), [{X: 'b', Y: 'c'}])
assert_equal(ts.query(['a X X']), no)
assert_equal(ts.query(['A B C']), [{A: 'a', B: 'b', C: 'c'}])
ts.add(['same c c', 'a b e'])
assert_equal(ts.query(['a b c']), yes)
assert_equal(ts.query(['a b c', 'a b e']), yes)
assert_equal(ts.query(['a b e', 'a b c']), yes)
assert_equal(ts.query(['a b d', 'a b c']), no)
assert_equal(ts.query(['A B C']), [
{A: 'a', B: 'b', C: 'c'},
{A: 'a', B: 'b', C: 'e'},
{A: 'same', B: 'c', C: 'c'},
])
assert_equal(ts.query(['A B C', 'A b c']), [
{A: 'a', B: 'b', C: 'c'},
{A: 'a', B: 'b', C: 'e'}])
assert_equal(ts.query(['a b X']), [{X: 'c'}, {X: 'e'}])
assert_equal(ts.query(['X Y Y']), [{X: 'same', Y: 'c'}])
assert_equal(ts.query(['A B C', 'X C Y']), [
{A: 'a', B: 'b', C: 'c', X: 'same', Y: 'c'},
{A:"same", B:"c", C:"c", X:"same", Y:"c"}])
},
triplestore_bits: function() {
var ts = new triplestore()
ts.add(['bit bit 0', 'bit bit 1'])
assert_equal(ts.query(['bit bit B0', 'bit bit B1', 'bit bit B2']),
[{B0:"0", B1:"0", B2:"0"}, {B0:"0", B1:"0", B2:"1"},
{B0:"0", B1:"1", B2:"0"}, {B0:"0", B1:"1", B2:"1"},
{B0:"1", B1:"0", B2:"0"}, {B0:"1", B1:"0", B2:"1"},
{B0:"1", B1:"1", B2:"0"}, {B0:"1", B1:"1", B2:"1"}])
},
};
</script>
<script>
// DHTML bits for this sample page
function tablerow(contents, type) {
if (!type) type = 'td'
var el = document.createElement('tr')
for (var ii = 0; ii < contents.length; ii++) {
var td = document.createElement(type)
td.appendChild(document.createTextNode(contents[ii]))
el.appendChild(td)
}
return el
}
function clear_node(nod) {
nod.innerHTML = ''
// Mozilla erroneously caches some layout... fix it
var old_display = nod.style.display
nod.style.display = 'none'
nod.style.display = old_display
}
function perform_query(qel, ts, triples) {
clear_node(qel)
var headers = freevars(map(split_triple, triples))
qel.appendChild(tablerow(headers, 'th'))
var data = ts.query(triples)
for (var ii in data)
qel.appendChild(tablerow(map(function(propname){return
data[ii][propname]}, headers)))
}
var ts = new triplestore()
function handle_input(somestuff) {
var triples = somestuff.split(/,\s*/)
var free = freevars(map(split_triple, triples))
if (free.length) {
perform_query(document.getElementById('querytable'), ts, triples)
} else {
// add to triple store
var tb = document.getElementById('stufftable')
for (var ii in triples)
tb.appendChild(tablerow(split_triple(triples[ii])))
ts.add(triples)
}
return false
}
function set_up_test_stuff() {
handle_input('beatrice parent aggie, beatrice spouse kragen, kragen spouse
beatrice, beatrice parent walter, kragen parent greg')
handle_input('aggie spouse walter, greg spouse paula')
var query = 'Person spouse Spouse, Spouse parent In-law, In-law spouse
In-law-2'
var field = document.getElementById('theform').stuff
field.value = query
handle_input(query)
field.focus()
}
</script>
</head>
<body onload="barebones_run_tests();set_up_test_stuff()"><h1>Toy triple store
in DHTML</h1>
<p> Inspired by Sniki.</p>
<table id="stufftable">
<tr><th>object</th><th>property</th><th>value</th></tr>
</table>
<table id="querytable">
</table>
<form id="theform" onsubmit="return handle_input(this.stuff.value)">
<input size="80" name="stuff" />
</form>
</body>
</html>