git » wikiri » commit 41037c5

Initial commit

author Alberto Bertogli
2007-12-30 05:33:37 UTC
committer Alberto Bertogli
2007-12-30 05:33:37 UTC

Initial commit

Signed-off-by: Alberto Bertogli <albertito@gmail.com>

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())
+
+