author | Alberto Bertogli
<albertito@gmail.com> 2007-12-30 05:33:37 UTC |
committer | Alberto Bertogli
<albertito@gmail.com> 2007-12-30 05:33:37 UTC |
LICENSE | +33 | -0 |
README | +46 | -0 |
config.py.sample | +25 | -0 |
wikiri.cgi | +580 | -0 |
diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d716d43 --- /dev/null +++ b/LICENSE @@ -0,0 +1,33 @@ + +I don't like licenses, because I don't like having to worry about all this +legal stuff just for a simple piece of software I don't really mind anyone +using. But I also believe that it's important that people share and give back; +so I'm placing this work under the following license, so you feel guilty if +you don't ;) + + +BOLA - Buena Onda License Agreement (v1.0) +------------------------------------------ + +This work is provided 'as-is', without any express or implied warranty. In no +event will the authors be held liable for any damages arising from the use of +this work. + +To all effects and purposes, this work is to be considered Public Domain. + + +However, if you want to be "Buena onda", you should: + +1. Not take credit for it, and give proper recognition to the authors. +2. Share your modifications, so everybody benefits from them. +4. Do something nice for the authors. +5. Help someone who needs it: sign up for some volunteer work or help your + neighbour paint the house. +6. Don't waste. Anything, but specially energy that comes from natural + non-renewable resources. Extra points if you discover or invent something + to replace them. +7. Be tolerant. Everything that's good in nature comes from cooperation. + +The order is important, and the further you go the more "Buena onda" you are. +Make the world a better place: be "Buena onda". + diff --git a/README b/README new file mode 100644 index 0000000..e7b8144 --- /dev/null +++ b/README @@ -0,0 +1,46 @@ + +wikiri - A single-file wiki engine +================================== + +wikiri is a single-file blog engine, written in Python_ and using +reStructuredText_ for markup. + +It's licenced under the BOLA_ license, which is pretty much the same as public +domain. Read the *LICENSE* file for more information. + + +Installing wikiri +----------------- + +First of all, you need a webserver. Put ``wikiri.cgi`` in a directory where +CGI is allowed. + +Then, create a data directory, where you will store your articles. + +Finally, configure wikiri by either copying the ``config.py.sample`` as +``config.py`` to the same directory where you put ``wikiri.cgi``, or editing +the values inside ``wikiri.cgi``. The former is recommended to simplify +updates. + +You can now browse the page to get to the index page. + + +Personalizing templates +----------------------- + +If you don't like the default look, you can write your own templates for +wikiri. This needs to be properly documented, but it's very obvious when you +look at the code. + + +Complaints and suggestions +-------------------------- + +If you have any questions, suggestions or comments, please send them to me, +Alberto Bertogli, at albertito@gmail.com. + + +.. _Python: http://www.python.org/ +.. _reStructuredText: http://docutils.sourceforge.net/rst.html +.. _BOLA: http://auriga.wearlab.de/~alb/bola/ + diff --git a/config.py.sample b/config.py.sample new file mode 100644 index 0000000..962c3de --- /dev/null +++ b/config.py.sample @@ -0,0 +1,25 @@ +#coding: utf8 + +# This is the sample configuration file for a blitiri blog engine. +# If you omit a variable, the default will be used. +# +# If you prefer, you can set the values directly inside blitiri.cgi and not +# have a configuration file. + +# Directory where entries are stored +data_path = "data/" + +# Path where templates are stored. Use an empty string for the built-in +# default templates. If they're not found, the built-in ones will be used. +templates_path = "templates/" + +# URL to the wiki, including the name. Can be a full URL or just the path. +wiki_url = "/wiki/wikiri.cgi" + +# Style sheet (CSS) URL. Can be relative or absolute. To use the built-in +# default, set it to wiki_url + "/style". +css_url = wiki_url + "/style" + +# Wiki title +title = "I like wikis" + diff --git a/wikiri.cgi b/wikiri.cgi new file mode 100755 index 0000000..a75ed19 --- /dev/null +++ b/wikiri.cgi @@ -0,0 +1,580 @@ +#!/usr/bin/env python +#coding: utf8 + +# wikiri - A single-file wiki engine. +# Alberto Bertogli (albertito@gmail.com) + +# +# Configuration section +# +# You can edit these values, or create a file named "config.py" and put them +# there to make updating easier. The ones in config.py take precedence. +# + +# Directory where entries are stored +data_path = "data/" + +# Path where templates are stored. Use an empty string for the built-in +# default templates. If they're not found, the built-in ones will be used. +templates_path = "templates/" + +# URL to the wiki, including the name. Can be a full URL or just the path. +wiki_url = "/wiki/wikiri.cgi" + +# Style sheet (CSS) URL. Can be relative or absolute. To use the built-in +# default, set it to wiki_url + "/style". +css_url = wiki_url + "/style" + +# Wiki title +title = "I like wikis" + +# +# End of configuration +# DO *NOT* EDIT ANYTHING PAST HERE +# + + +import sys +import os +import errno +import datetime +import urllib +import cgi + +# Load the config file, if there is one +try: + from config import * +except: + pass + + +# Find out our URL, just in case the templates need it (the default templates +# do not use it at all) +try: + n = os.environ['SERVER_NAME'] + p = os.environ['SERVER_PORT'] + s = os.environ['SCRIPT_NAME'] + if p == '80': p = '' + else: p = ':' + p + full_url = 'http://%s%s%s' % (n, p, s) +except KeyError: + full_url = 'Not needed' + + + +# +# Markup processing +# + +import re +import docutils.parsers.rst +import docutils.nodes +from docutils.core import publish_parts + +def _wiki_link_role(role, rawtext, text, lineno, inliner, + options = {}, content = []): + + ref = wiki_url + '/' + \ + urllib.quote_plus(text.encode('utf8'), safe = '') + + node = docutils.nodes.reference(rawtext, text, refuri = ref, + **options) + return [node], [] + +def content2html(content): + settings = { + 'input_encoding': 'utf8', + 'output_encoding': 'utf8', + } + + docutils.parsers.rst.roles.register_canonical_role('wikilink', + _wiki_link_role) + docutils.parsers.rst.roles.DEFAULT_INTERPRETED_ROLE = 'wikilink' + + parts = publish_parts(content, + settings_overrides = settings, + writer_name = "html") + return parts['body'].encode('utf8') + + + +# +# Templates +# + +# Default template + +default_article_content = """\ +title: %(artname)s + +This page does *not* exist yet. +""" + +default_main_header = """ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> + +<html> +<head> +<link href="%(css_url)s" rel="stylesheet" type="text/css" /> +<title>%(title)s</title> +</head> + +<body> + +<h1><a href="%(url)s">%(title)s</a></h1> + +<div class="content"> +""" + +default_main_footer = """ +</div><p/> +</div> + +</body> +</html> +""" + +default_article_header = """ +<div class="article"> +<h2><a href="%(url)s/%(artqname)s">%(arttitle)s</a></h2> +<span class="artinfo"> + updated on %(uyear)04d-%(umonth)02d-%(uday)02d + %(uhour)02d:%(uminute)02d<br/> +</span><br/> +<p/> +<div class="artbody"> +""" + +default_article_footer = """ +<p/> +</div> +<hr/><p/> +<div class="footer"> + <a href="%(url)s/help">help</a> | + <a href="%(url)s/%(artqname)s/edit">edit this page</a><br/> +</div> +</div> +""" + +default_edit_page = """ +<h2>Edit <a href="%(url)s/%(artqname)s">%(artname)s</a></h2> +<p/> + +<div class="editpage"> +<form method="POST" action="save"> +<label for="newtitle">Title: </label> +<input id="newtitle" type="text" name="newtitle" + value="%(arttitle)s"/><p/> +<textarea name="newcontent" cols="76" rows="25"> +%(content)s +</textarea><p/> + +<input type="submit" value="Save"/> +</form> +<p/> +</div> + +<div class="quickhelp"> +<h2>Quick help</h2> + +<ul> + <li>Paragraphs are split by an empty line.</li> + <li>Lines that are underlined with dashes ("----") are a title.</li> + <li>Lines beginning with an "*" form a list.</li> + <li>To create new links, put them between backticks, <tt>`like + this`</tt>.</li> +</ul> + +More information: +<a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">reST +quick reference</a>.<p/> +</div> + +</div> +""" + +default_removed_page = """ +<div class="article"> +<h2><a href="%(url)s/%(artqname)s">%(artname)s</a></h2> +<div class="artbody"> +<p/> +This page has been successfuly removed.<p/> + +</div> +</div> +""" + +default_help_page = """ +<div class="article"> +<h2>Help</h2> +<div class="artbody"> +<p/> + +This wiki uses <a href="http://docutils.sf.net/rst.html">reStructuredText</a> +for markup.<br/> +Here is a quick syntax summary.<p/> + +<ul> + <li>Paragraphs are split by an empty line.</li> + <li>Lines that are underlined with dashes ("----") are a title.</li> + <li>Lines beginning with an "*" form a list.</li> + <li>To create new links, put them between backticks, <tt>`like + this`</tt>.</li> +</ul> + +If you want more information, see the +<a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">reST +quick reference</a>.<p/> + +</div> +</div> +""" + +# Default CSS +default_css = """ +body { + font-family: sans-serif; +} + +div.content { + width: 60%; +} + +h1 { + font-size: x-large; + border-bottom: 2px solid #99F; + width: 65%; + margin-bottom: 1em; +} + +h2 { + font-size: large; + font-weigth: none; + margin-bottom: 1pt; + border-bottom: 1px solid #99C; +} + +h1 a, h2 a { + text-decoration: none; + color: black; +} + +span.artinfo { + font-size: x-small; +} + +span.artinfo a { + text-decoration: none; + color: #339; +} + +span.artinfo a:hover { + text-decoration: none; + color: blue; +} + +div.artbody { + margin-left: 1em; +} + +div.article { + margin-bottom: 2em; +} + +hr { + /* hack to properly align the hr */ + text-align: left; + margin: 0 auto 0 0; + + height: 2px; + border: 0; + background-color: #99F; + width: 60%; +} + +div.footer { + font-size: small; +} + +div.footer a { + text-decoration: none; +} + +/* Articles are enclosed in <div class="section"> */ +div.section h1 { + font-size: medium; + font-weigth: none; + width: 100%; + margin-bottom: 1pt; + border-bottom: 1px dotted #99C; +} + +""" + +class Templates (object): + def __init__(self): + self.tpath = templates_path + now = datetime.datetime.now() + + self.vars = { + 'css_url': css_url, + 'title': title, + 'url': wiki_url, + 'fullurl': full_url, + 'year': now.year, + 'month': now.month, + 'day': now.day, + } + + def art_vars(self, article): + avars = self.vars.copy() + avars.update( { + 'arttitle': article.title, + 'artname': article.name, + 'artqname': article.qname, + 'updated': article.updated.isoformat(' '), + + 'uyear': article.updated.year, + 'umonth': article.updated.month, + 'uday': article.updated.day, + 'uhour': article.updated.hour, + 'uminute': article.updated.minute, + 'usecond': article.updated.second, + } ) + return avars + + def get_main_header(self): + p = self.tpath + '/header.html' + if os.path.isfile(p): + return open(p).read() % self.vars + return default_main_header % self.vars + + def get_default_article_content(self, artname): + avars = self.vars.copy() + avars.update( { + 'artname': artname, + 'artqname': urllib.quote_plus(artname, safe = ''), + } ) + p = self.tpath + '/default_article_content.wiki' + if os.path.isfile(p): + return open(p).read() % avars + return default_article_content % avars + + def get_main_footer(self): + p = self.tpath + '/footer.html' + if os.path.isfile(p): + return open(p).read() % self.vars + return default_main_footer % self.vars + + def get_article_header(self, article): + avars = self.art_vars(article) + p = self.tpath + '/art_header.html' + if os.path.isfile(p): + return open(p).read() % avars + return default_article_header % avars + + def get_article_footer(self, article): + avars = self.art_vars(article) + p = self.tpath + '/art_footer.html' + if os.path.isfile(p): + return open(p).read() % avars + return default_article_footer % avars + + def get_edit_page(self, article): + avars = self.art_vars(article) + avars.update( { + 'content': article.raw_content, + } ) + p = self.tpath + '/edit_page.html' + if os.path.isfile(p): + return open(p).read() % avars + return default_edit_page % avars + + def get_removed_page(self, artname): + avars = self.vars.copy() + avars.update( { + 'artname': artname, + 'artqname': urllib.quote_plus(artname, safe = ''), + } ) + p = self.tpath + '/removed_page.html' + if os.path.isfile(p): + return open(p).read() % avars + return default_removed_page % avars + + def get_help_page(self): + p = self.tpath + '/help_page.html' + if os.path.isfile(p): + return open(p).read() % self.vars + return default_help_page % self.vars + + + +# +# Article handling +# + +class Article (object): + def __init__(self, name): + self.name = name + self.qname = urllib.quote_plus(name, safe = "") + self.updated = None + + self.loaded = False + + # loaded on demand + self.attrs = {} + self._raw_content = '' + + def get_raw_content(self): + if not self.loaded: + self.load() + return self._raw_content + raw_content = property(fget = get_raw_content) + + def get_title(self): + if not self.loaded: + self.load() + # use the name by default + return self.attrs.get('title', self.name) + title = property(fget = get_title) + + def load(self): + try: + fd = open(data_path + '/' + self.qname) + raw = fd.readlines() + stat = os.fstat(fd.fileno()) + self.updated = datetime.datetime.fromtimestamp(stat.st_mtime) + except: + t = Templates() + raw = t.get_default_article_content(self.name) + raw = [ s + '\n' for s in raw.split('\n') ] + self.updated = datetime.datetime.now() + + count = 0 + for l in raw: + if ':' in l: + name, value = l.split(':', 1) + name = name.lower().strip() + self.attrs[name] = value.strip() + + elif l == '\n': + # end of header + break + count += 1 + self._raw_content = ''.join(raw[count + 1:]) + self.loaded = True + + def save(self, newtitle, newcontent): + fd = open(data_path + '/' + self.qname, 'w+') + fd.write('title: %s\n\n' % newtitle) + fd.write(newcontent) + fd.close() + + def remove(self): + try: + os.unlink(data_path + '/' + self.qname) + except OSError, errno.ENOENT: + pass + + def to_html(self): + return content2html(self.raw_content) + + + +# +# Main +# + +def render_article(art): + template = Templates() + print 'Content-type: text/html; charset=utf-8\n' + print template.get_main_header() + print template.get_article_header(art) + print art.to_html() + print template.get_article_footer(art) + print template.get_main_footer() + +def render_edit(art): + template = Templates() + print 'Content-type: text/html; charset=utf-8\n' + print template.get_main_header() + print template.get_edit_page(art) + print template.get_main_footer() + +def render_removed(artname): + template = Templates() + print 'Content-type: text/html; charset=utf-8\n' + print template.get_main_header() + print template.get_removed_page(artname) + print template.get_main_footer() + +def render_help(): + template = Templates() + print 'Content-type: text/html; charset=utf-8\n' + print template.get_main_header() + print template.get_help_page() + print template.get_main_footer() + +def render_style(): + print 'Content-type: text/plain\n' + print default_css + +def handle_cgi(): + import cgitb; cgitb.enable() + + form = cgi.FieldStorage() + + edit = False + save = False + artname = 'index' + + newcontent = form.getfirst("newcontent", '') + newtitle = form.getfirst("newtitle", '').strip() + + if os.environ.has_key('PATH_INFO'): + path_info = os.environ['PATH_INFO'] + path_info = os.path.normpath(path_info) + + edit = path_info.endswith('/edit') + save = path_info.endswith('/save') + + if edit or save: + artname = path_info[1:].rsplit('/', 1)[0] + else: + artname = path_info[1:] + + if artname == '' or artname == '/': + artname = 'index' + + + artname = urllib.unquote_plus(artname) + + if artname == 'style': + render_style() + elif artname == 'help': + render_help() + elif edit: + render_edit(Article(artname)) + elif save: + if newcontent.strip(): + Article(artname).save(newtitle, newcontent) + render_article(Article(artname)) + else: + Article(artname).remove() + render_removed(artname) + else: + render_article(Article(artname)) + + +def handle_cmd(): + print "This is a CGI application." + print "It only runs inside a web server." + return 1 + + +if os.environ.has_key('GATEWAY_INTERFACE'): + handle_cgi() +else: + sys.exit(handle_cmd()) + +