NOTE: This software is occasionally a work in progress.

This is a simple example of a wiki created with web.py. All pages are stored locally as plaintext files. If a folder or file doesn't exist the first time you run simplewiki.py, it will be created. The code in some places is ugly, as far as readability goes, but one goal I had in writing this was to limit the total lines of code necessary. See the original Wiki Principles and Wiki Design Principles for a brief overview of what a "wiki" could/should/can/might include.

This tool was created as a fun experiment in shrinking code volume and as a useful, portable tool I could move between machines. Not every system I have access to has always-on internet, so a long running server process is out. Additionally, I move between operating systems and permission levels, so relying on a database for storage is also out. With simplewiki.py I zip up the main folder and email myself the resulting file. web.py is super-portable, so I don't have any problems running the wiki as localhost wherever I'm at.

simplewiki.py

    #!/usr/bin/python
    # Simplistic wiki for localhost, by Adam Bachman
    import web, time, os
    from markdown import markdown

    urls = ('/([a-zA-Z]*)', 'view', '/_([a-zA-Z]*)', 'edit')

    class view:
        def GET(self, name):
            name = name or 'index'
            print render.view(name, getpage(name) or "%s doesn't exist"%name)

    class edit:
        def GET(self, name):
            print render.edit(name, getpage(name) or "edit text here")

        def POST(self, name):
            if iscur(name): write(BAK+tname(name), getpage(name))
            write(CUR+name, web.input().page_text)
            web.seeother('/'+name)

    ## os related utilities
    CUR = 'current/'; BAK = 'backup/'; TMPL = 'templates/'
    exists = os.path.exists
    iscur = lambda n: exists(CUR+n)
    mkdirs = lambda dl: [os.mkdir(d) for d in dl if not exists(d)]
    mkdirs([CUR,BAK,TMPL])

    ## file based read / write
    tname = lambda n: n+'.'+str(int(time.time()))
    getpage = lambda n: iscur(n) and file(CUR+n, 'r').read() or None
    write = lambda n, t: file(n, 'w').write(t)

    ## text formatting (links, includes, markdown)
    cc = web.re_compile('([A-Z][a-z]*[A-Z]+[a-z]+[a-zA-Z]*)')
    inc = web.re_compile('(?<!\\\){{([A-Z][a-z]*[A-Z]+[a-z]+[a-zA-Z]*)}}')
    incify = lambda m: str(getpage(m.groups()[0]))+'\n\n- - - - - \n\n'
    _linkify = lambda n: iscur(n) and '[%s](/%s)'%(n,n) or '%s[?](/_%s)'%(n,n)
    linkify = lambda m: _linkify(m.group())
    htmlize = lambda t: str(markdown(cc.sub(linkify,inc.sub(incify, t))))

    if not exists('templates/view.html'): # UGLY, included for convenience
        view=('$def with (name, text)\n<html><head><title>$name </title></head><b'
        'ody><h1><a href="/">@</a> <a href="/_$name">$name </a></h1>$:htmlize(te'
        'xt)\n</body></html>')
        edit=('$def with (name, text)\n<html><head><title>$name </title></head><b'
        'ody><a style="cursor:pointer;" onclick="history.back()"><h1>Editing: $na'
        'me </h1></a><form action="" method="post"><textarea name="page_text" row'
        's="25" style="width:100%">$text</textarea><br /><input type="submit" val'
        'ue="Submit" /></form></body></html>')
        write(TMPL+'view.html', view); write(TMPL+'edit.html',edit)

    render = web.template.render(TMPL)
    web.template.Template.globals['htmlize'] = htmlize

    if __name__=="__main__":
        web.internalerror = web.debugerror
        web.run(urls, globals(), web.reloader)

goals:

requires:

features:

limitations:

how does it work?

When you first run simplewiki.py, three new folders and a couple of new files will be created.

current/ and backup/ hold the plain text wiki pages you will create and templates/ holds the two template files that will be rendered into your wiki pages.

When first visiting http://localhost:8080/, you'll be greeted with an empty textarea (editing box). Fill it in, click submit, and you've got a starting page ("index") for your wiki.

Everytime you edit a page you can create links to new (uncreated) pages by writing words in CamelCase. To make a CamelCase word, just mash a string of words together (two or more), capitalizing the first letter of all the words. When you save the page, each of your CamelCase words will have a [?]() following it, linking directly to the editing screen for that page. If they already exist, the CamelCase word itself will become a link to view the page.

To edit any page that already exists, click the title. To edit a page without linking to it, type the address by hand. Example: http://localhost:8080/anewpage

All text you enter will be processed using markdown. See the markdown syntax page for info.

directory map:

  simple-wiki/
    simplewiki.py

  (created at first run)
    templates/
      view.html
      edit.html
    current/
    backup/

some python tricks I learned on the way:

    lambda

This fellow turns an expression into a function. It returns whatever the expression evaluates to. It's more complex than that, I'm sure, but the simplistic definition works for me.

    view=('$def with (name, text)\n<html><head><title>$name </title></head><b'
    'ody><h1><a href="/">@</a> <a href="/_$name">$name </a></h1>$:htmlize(te'
    'xt)\n</body></html>')

Enclosing multiple lines of text in single quotes and enclosing the whole thing in parentheses is similar to triple quoting long stings but avoids adding newlines when the resulting string is evaluated. If I used triple quotes in this case I'd have to break lines irregularly in simplewiki.py to avoid screwing up the resulting html.

    print render.edit(name, getpage(name) or "edit text here")

When sending arguments to render.edit, the or keyword forces python to evaluate the given statement before passing the arguement (at least that's how I understand it to work). If getpage evaluates to None (because the page doesn't exist), "edit text here" is passed instead. If a page already exists, python doesn't evaluate the rest of the or statement and the "edit..." string is ignored. Similarly getpage = lambda n: iscur(n) and file(CUR+n, 'r').read() or None uses the expanded version--the 'and or' trick--to return the text of a file or None depending on whether iscur() returns True or False, respectively.

    htmlize

... relies on python's regular expression tool's ability to use a function instead of a string when performing regex substitutions. Here's a simple example:

    >>> print re.sub('([a-z]*)', r'_\1_', 'as the')
    '_as_ _the_'

uses a string literal as the sustitution string. Using a function instead of a string, I could say:

    >>> _wd_ = lambda mob: '_' + mob.group() + '_'
    >>> print re.sub('([a-z]*)', _wd_, 'as the')
    '_as_ _the_'

The substitution function (_wd_) is passed the match object for each unique match. Once control has been passed to the substitution function, it's a free for all. The functions controlling includes and CamelCase link substitutions are built using this method. htmlize links these together along with markdown to transform your plaintext into html in one fell swoop.

the future:

The simple wiki is an interesting starting point for realizing more specific applications. As mentioned in the features section, a blog or todo list can be created by 'including' individual pages in a single main page. Automate the appending of pages everytime a new page is created and you're 90% there. What other kinds of apps could be developed using a wiki as a foundation?

The existing page data model is extremely simple (name, contents), what kind of transformations would be needed to move from a wiki to a todo list? How about from a wiki to a cookbook? Can these transformations be performed without moving away from the simplicity and portability intended by simplewiki's flat file storage?