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:
- simple as possible, minimal wiki featureset (read/write, link, format)
- portable and easy to expand, all data stored as plaintext files.
- concise as possible, less code.
requires:
- web.py (> 0.2)
- markdown.py
features:
- create new pages simply. editing a page creates it, linking to a page lets you edit it.
- automatic linking. Simply spell a word using CamelCase (mixed caps) and it will be replaced with a link to the page. If the page doesn't exist, you'll see a '?' link to edit the page.
- simple includes. Wrap any valid page name in double curly braces ({{APage}}) while editing and the text of that page will be included inline when your recently edited page is displayed. Together with CamelCase links, simple includes give you the ability to re-create a lot of interesting web-style linked data types in your wiki. Create a blog or todo list by including many pages in a single page, avoid copying and pasting between multiple pages or come up with a new use.
- simple plaintext markup using markdown formatting.
- automatic backups, save every version of every page.
- no users, no editing controls, you own the system.
limitations:
- on file systems without case sensitive filenames (Windows), APage is the same as APAge.
- A reliable CamelCase regular expression is hard to perfect, mine's decent. All WikiWords used must start with a capital letter, and have at least one lowercase letter following the second uppercase letter in the word. THis is a link, ThiS is not, neither is THIS or tHIs.
- If you've got a lot of pages (A LOT), your backup folder may get big. I just comment out the backup line and avoid worrying about the past.
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?

