Inspired by Personal Command Line Everything, I wrote a simple Everything-like engine that runs as a private server on your computer, accessible through your browser. You'll need Python to run it. The default URL to access it is "http://localhost:8000". I haven't tested it on Windows, just Ubuntu Linux. /msg me with bugs or feature requests, although I don't plan on adding anything too complicated.
It is like E2 in some ways, and in other ways it is not. The default 'home' node has more information about how it all works (it's the first node you will arrive at upon connecting). If you're looking for something more scratch-pad-like, check out JS Scratch Pad.
Changes:
- added 'delete'
- added search box (probably multiplied number of bugs by ten)
- added database switching
- per-paragraph editing ← drastic change, let me know what you think
- added softlinks (ush's reminder)
- fixed html tag matching, html entities
#!/usr/bin/python
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
from urllib import quote, unquote_plus
import sqlite3, os, sys, re, time
PORT = 8000
DBNAME = 'default.db'
# connect to DB, init if necessary
def loadDB():
global DB
print >> sys.stderr, 'Loading DB:', DBNAME
needsInit = not os.path.exists(DBNAME)
DB = sqlite3.connect(DBNAME)
if needsInit:
print >> sys.stderr, 'Initializing DB:', DBNAME
cur = DB.cursor()
cur.execute('create table node (title varchar(255) unique, body varchar(65535), time integer)')
cur.execute('create table softlink (title1 varchar(255), title2 varchar(255), rank integer)')
cur.execute('create index idx_softlink on softlink (title1, title2)')
cur.close()
postNode('home', 0, DEFAULT_HOME_NODE)
DEFAULT_HOME_NODE = r'''
<html>
Welcome to your Personal Everything!
<noscript><br><br><span style="color: red">Javascript is off! You'll need it to edit nodes.</span></noscript>
<p>
Paragraphs/sections are individually editable and are separated by <p> on a line by itself. Once they are saved, you won't be able to see the <p> any more, but it's there. <b>To create new paragraphs</b>, just add one or more <p>s where desired, followed by text. When you save the paragraph, you will see the new paragraphs separately. <b>To delete a paragraph</b>, just remove all of its text and save it. Empty paragraphs are automatically removed.
<p>
Use brackets to create [links] (you enter: \[links]) and [this is a pipelink|pipelinks] (you enter: \[this is a pipelink|pipelinks]). To enter actual brackets, you can either use the HTML entities &#91; and &#93; or put a \ before any left brackets: \\[like this]
Links to non-existent nodes will be displayed in [There is probably no node with this title.|red]. Links beginning with http:// will be identified as external links, like so: [http://www.google.com]. (The \[^] marks them as being external, even when [http://www.google.com|pipelinked].)
<p>
Allowed HTML tags: b, i, u, s, sup, sub, big, small, tt.
For more HTML, put <html> at the very beginning of a paragraph. This will make the whole paragraph raw HTML (and hence disable linking and automatic line-breaking). Be careful with this, as it is probably possible to screw up a node accidentally with bad HTML.
'''
def htmlSafe(s):
return s.replace('&','&').replace('<','<').replace('>','>')
def unhtmlSafe(s):
return s.replace('>','>').replace('<','<').replace('&','&')
def decodeQuery(s):
d = {}
for kv in s.split('&'):
if '=' not in kv: continue
k, v = kv.split('=', 1)
d[k] = unquote_plus(v)
return d
def linker(m, softlink = None):
style = ''
if type(m) == str or type(m) == unicode: dest = text = str(m)
else: dest = text = m.group(1)
if '|' in text: dest, text = text.split('|', 1)
if dest[:7].lower() == 'http://': link = dest; text += '[^]'
else:
link = '/node/' + dest
if softlink and dest != softlink:
link += '?softlink=' + quote(softlink)
cur = DB.cursor()
cur.execute('select time from node where title=?', (dest,))
if not cur.fetchone(): style = 'style="color: red" '
cur.close()
return '<a %stitle="%s" href="%s">%s</a>' % (style, dest, link, text)
RE_LINKS = re.compile(r'(?<!\\)\[(.{,512}?)\]')
RE_ALLOWED_TAGS = re.compile(r'<([bius]|sup|sub|big|small|tt)>(.*?)</\1>', re.I | re.DOTALL)
RE_ALLOWED_TAGS_FUNC = lambda m: '<%s>%s</%s>' % (m.group(1), m.group(2), m.group(1))
RE_ALLOWED_ENTITIES = re.compile(r'&(#\d+|[a-zA-Z0-9]+);')
def getNode(title):
body = getNodeBody(title)
exists = (body != None)
if not exists: body = []
display = '''\
<style>
form { margin-bottom: 0px; }
.linktable { width: 100%; border: 1px solid #ddddff; border-spacing: 0; }
.linktable td { text-align: center; padding: 5px; background-color: #fafaff; border: 1px solid #eeeeff; }
</style>
<script>
var editingPara = -1;
var editTemp;
function mouseOver(eid) {
var el = document.getElementById('para' + eid);
el.style.backgroundColor = 'lightblue';
}
function mouseOut(eid) {
var el = document.getElementById('para' + eid);
el.style.backgroundColor = 'transparent';
}
function mouseClick(eid) {
if (editingPara != -1) return;
editingPara = eid;
var el = document.getElementById('para' + eid);
var cel = document.getElementById('editpara' + eid);
editTemp = el.innerHTML;
el.innerHTML = '<form method="POST"><input type="hidden" name="para" value="' + eid + '"><textarea name="text" style="width: 100%; height: 200px">' + cel.innerHTML + '</textarea><br><table style="width: 100%"><tr><td align=left><input name="save" type="submit" value="save"></td><td align=right><input onclick="cancelEdit(' + eid + ')" type="button" value="cancel"></td></tr></table></form>';
}
function cancelEdit(eid) {
if (editingPara == -1) return;
var el = document.getElementById('para' + eid);
var cel = document.getElementById('editpara' + eid);
el.innerHTML = editTemp;
el.style.backgroundColor = 'transparent';
editingPara = -1;
}
</script>'''
pi = 0
for p in body:
clean = p
# html mode
if p[:12] == '<html>':
p = unhtmlSafe(p[12:])
else:
# links and brackets
p = RE_LINKS.sub(lambda x: linker(x, title), p)
p = p.replace(r'\[', '[')
# html tags
oldp = None
while oldp != p:
oldp = p
p = RE_ALLOWED_TAGS.sub(RE_ALLOWED_TAGS_FUNC, p)
# html entities
p = RE_ALLOWED_ENTITIES.sub(lambda m: '&%s;' % m.group(1), p)
# automatic linebreaks
p = re.sub(r'\r?\n', '<br>', p)
# paragraph magic
display += ('<div id="para%i" style="margin: 4px; padding: 4px' + ('; padding-top: 8px' if pi else '') + '"><span style="float: right" onmouseover="mouseOver(%i)" onmouseout="mouseOut(%i)"><input type="button" value="edit" onclick="mouseClick(%i)"></span>').replace('%i', str(pi)) + p + '</div><div id="editpara%i" style="display: none">' % pi + clean + '</div>'
pi += 1
if not len(body):
display += '<div id="para0"></div><div id="editpara0" style="display: none"></div>'
display += '<script> mouseClick(0); editingPara = -1; </script>'
# get softlinks
cur = DB.cursor()
cur.execute('select title1, title2 from softlink where title1=? or title2=? order by rank desc limit 30', (title, title))
display += niceLinkTable(title, [t[0] if t[1] == title else t[1] for t in cur.fetchall()])
# get latest changed nodes
cur.execute('select title from node order by time desc limit 30')
latest = niceLinkTable(title, [x[0] for x in cur.fetchall()], 'Recent Nodes')
# lay out the page (maybe eventually generate this with some pretty templating...)
cur.close()
return '''\
<table style="width: 100%; border-bottom: 1px dotted black"><tr>
<td><big><big><b>''' + title + '''</b></big></big></td>
<td align=center>''' + ('<a href="?op=delete">delete</a>' if exists else '') + '''</td>
<td align=center><small>database: ''' + DBNAME + '''<br><a href="/setdb">change</a></small></td>
<td align=right><form method="GET" action="/search">
<input name="q"><br>
<input type="hidden" name="softlink" value="''' + title + '''">
<input type="checkbox" name="ignoreexact"><small> ignore exact</small>
</form></td>
</tr></table>
<div style="background-color: #EEEEEE; margin: 8px; padding: 1px">''' + display + '''</div>
''' + latest
def getNodeBody(title):
cur = DB.cursor()
cur.execute('select body from node where title=?', (title,))
body = cur.fetchone()
cur.close()
if not body: return None
lst = [s.strip() for s in body[0].split('<p>')]
while '' in lst: lst.remove('')
return lst
def postNode(title, pi, text):
body = getNodeBody(title)
if body == None: body = []
body[pi:pi+1] = map(htmlSafe, re.split(r'\r?\n<p>\r?\n', text))
body = '<p>'.join(body)
if not len(body): return deleteNode(title)
cur = DB.cursor()
cur.execute('insert or replace into node (title, body, time) values (?,?,?)', (title, body, time.time()))
cur.close()
DB.commit()
def softlinkNode(title1, title2):
if title1 == title2: return
if title1 > title2: title1, title2 = title2, title1
cur = DB.cursor()
cur.execute('update softlink set rank=rank+1 where title1=? and title2=?', (title1, title2))
if cur.rowcount == 0: # create softlink
cur.execute('insert into softlink values (?,?,?)', (title1, title2, 1))
cur.close()
DB.commit()
def deleteNode(title):
cur = DB.cursor()
cur.execute('delete from node where title=?', (title,))
cur.close()
DB.commit()
def doSearch(curnode, query):
cur = DB.cursor()
results = {}
for term in query.split():
cur.execute('select title from node where title like ?', ('%%%s%%' % term,))
for tup in cur.fetchall():
if tup[0] not in results: results[tup[0]] = 1
else: results[tup[0]] += 1
cur.close()
if not len(results): return # this will redirect to the nodeshell
html = '<b>Search results for</b> <a href="/node/%s">%s</a><br><br>' % (query, query)
results = [(a, b) for b, a in results.items()]
results.sort(); results.reverse()
for rank, title in results:
html += linker(title, curnode)
return html
NICEBOX = '<div align=center style="border: 1px solid black; padding: 8px; margin: 8px">%s</div>'
def niceLinkTable(curnode, links, title = None):
if not len(links): return ''
html = '<table class="linktable"><tr>'; i = 0; multirow = False
if title: html += '<td colspan=5>%s</td></tr><tr>' % title
for ln in links:
html += '<td>' + linker(ln, curnode) + '</td>'; i += 1
if i == 5: html += '</tr><tr>'; i = 0; multirow = True
if multirow:
while i and i < 5: html += '<td>‌</td>'; i += 1
return html + '</tr></table>'
class PetHandler(BaseHTTPRequestHandler):
def do_GET(my): my.doAny()
def do_POST(my): my.doAny()
def doAny(my):
# variables used to write the response at the end of this func
code = 200
ctype = 'text/html'
headers = ''
data = ''
# parse query string
if '?' in my.path:
my.path, q = my.path.split('?', 1)
q = decodeQuery(q)
else: q = None
# get POST data if any
if my.command == 'POST':
clen = int(my.headers.get('Content-Length'))
post = decodeQuery(my.rfile.read(clen))
else: post = None
# action based on path
if my.path[:6] == '/node/':
title = unquote_plus(my.path[6:])
# replace or insert para
if post:
code = 303
headers += 'Location: /node/' + title
postNode(title, int(post['para']), post['text'])
# node deletion
elif q and 'op' in q and q['op'] == 'delete':
if 'confirm' in q and q['confirm'] == '1':
code = 303
headers += 'Location: /node/' + title
deleteNode(title)
else:
data = '<b>Delete node: %s</b><br><br>' % title
data += '<a href="/node/%s">no, do nothing</a><br><br>' % title
data += '<a href="?op=delete&confirm=1">yes, delete</a><br><br>'
data = NICEBOX % data
# softlinks
elif q and 'softlink' in q:
code = 303
headers += 'Location: /node/' + title
softlinkNode(q['softlink'], title)
# node retrieval
else: data = getNode(title)
elif my.path == '/search':
curnode = q['softlink']
if 'ignoreexact' not in q:
cur = DB.cursor()
cur.execute('select time from node where title=?', (q['q'],))
if cur.fetchone():
code = 303
headers += 'Location: /node/' + q['q'] + '?softlink=' + curnode
cur.close()
if code != 303:
data = doSearch(curnode, q['q'])
if not data:
data = ''
code = 303
# node probably doesn't exist
headers += 'Location: /node/' + q['q']
else: data = NICEBOX % data
elif my.path == '/setdb':
if q and 'db' in q:
global DBNAME
if q['db'][-3:] != '.db': q['db'] += '.db'
DBNAME = q['db']
loadDB()
code = 303
headers += 'Location: /node/home'
else:
data = '<b>Select database:</b><br><br>'
for fn in os.listdir('.'):
if fn[-3:] == '.db':
data += '<a href="?db=%s">%s</a><br>' % (fn, fn)
data += '<br><form method="GET"><input name="db"><input type="submit" value="create"></form>'
data = NICEBOX % data
else: # redirect to home node
code = 303
headers += 'Location: /node/home'
# write the response to the client
my.wfile.write('HTTP/1.1 %i \r\n' % code)
my.wfile.write('Content-Type: %s\r\n' % ctype)
my.wfile.write('Content-Length: %i\r\n' % len(data))
my.wfile.write(headers + '\r\n')
my.wfile.write(data)
if __name__ == '__main__':
loadDB()
HOST = 'localhost'
print >> sys.stderr, 'Starting server on http://%s:%i' % (HOST, PORT)
HTTPServer((HOST, PORT), PetHandler).serve_forever()