display | more...

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 &amp;#91; and &amp;#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('&','&amp;').replace('<','&lt;').replace('>','&gt;')
def unhtmlSafe(s):
        return s.replace('&gt;','>').replace('&lt;','<').replace('&amp;','&')


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'&lt;([bius]|sup|sub|big|small|tt)&gt;(.*?)&lt;/\1&gt;', 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'&amp;(#\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] == '&lt;html&gt;':
                        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>&zwnj;</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()

Log in or register to write something here or to contact authors.