git » blitiri » commit e1a4cc9

Auto-formatting with black

author Alberto Bertogli
2022-08-20 19:28:17 UTC
committer Alberto Bertogli
2022-08-20 19:30:49 UTC
parent cd5c3f32c4f97278384638165f1bc71806640f68

Auto-formatting with black

blitiri.cgi +930 -897

diff --git a/blitiri.cgi b/blitiri.cgi
index 3ca329f..69845ec 100755
--- a/blitiri.cgi
+++ b/blitiri.cgi
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-#coding: utf8
+# coding: utf8
 
 # blitiri - A single-file blog engine.
 # Alberto Bertogli (albertito@gmail.com)
@@ -27,7 +27,7 @@ templates_path = "/tmp/blog/templates"
 # Path where the cache is stored (must be writeable by the web server);
 # set to None to disable. When enabled, you must take care of cleaning it up
 # every once in a while.
-#cache_path = "/tmp/blog/cache"
+# cache_path = "/tmp/blog/cache"
 cache_path = None
 
 # URL to the blog, including the name. Can be a full URL or just the path.
@@ -56,21 +56,25 @@ index_articles = 10
 
 # Before rendering the rst, perform these regexp-based replacements.
 rst_regexp_replaces = [
-    (r'.. youtube:: (.*)',
-    r'''.. raw:: html
+    (
+        r".. youtube:: (.*)",
+        r""".. raw:: html
 
          <iframe width="560" height="315"
                  src="https://www.youtube.com/embed/\1"
                  frameborder="0" allowfullscreen></iframe>
-    '''),
-	(r'.. vimeo:: (\w*)',
-    r'''.. raw:: html
+    """,
+    ),
+    (
+        r".. vimeo:: (\w*)",
+        r""".. raw:: html
 
          <iframe src="https://player.vimeo.com/video/\1"
                  width="500" height="281" frameborder="0"
                  webkitallowfullscreen mozallowfullscreen
                  allowfullscreen></iframe>
-	'''),
+	""",
+    ),
 ]
 
 #
@@ -98,9 +102,9 @@ sys.path.append(os.getcwd())
 
 # Load the config file, if there is one
 try:
-	from config import *
+    from config import *
 except:
-	pass
+    pass
 
 
 # Pimp *_path config variables to support relative paths
@@ -121,9 +125,9 @@ templates_path = os.path.realpath(templates_path)
 # 	help -> a string with extra instructions, shown only when the user
 # 	        failed to solve the puzzle
 # Methods:
-#	validate(form_data) -> based on the form data[2],  returns True if
-#	                       the user has solved the puzzle uccessfully
-#	                       (False otherwise).
+# 	validate(form_data) -> based on the form data[2],  returns True if
+# 	                       the user has solved the puzzle uccessfully
+# 	                       (False otherwise).
 #
 # Note you must ensure that the puzzle attribute and validate() method can
 # "communicate" because they are executed in different requests. You can pass a
@@ -146,35 +150,38 @@ templates_path = os.path.realpath(templates_path)
 # 	body, body_error
 # 	action, method
 
-class TitleCaptcha (object):
-	"Captcha that uses the article's title for the puzzle"
-	def __init__(self, article):
-		self.article = article
-		words = article.title.split()
-		self.nword = hash(article.title) % len(words) % 5
-		self.answer = words[self.nword]
-		self.help = 'gotcha, damn spam bot!'
-
-	@property
-	def puzzle(self):
-		nword = self.nword + 1
-		if nword == 1:
-			n = '1st'
-		elif nword == 2:
-			n = '2nd'
-		elif nword == 3:
-			n = '3rd'
-		else:
-			n = str(nword) + 'th'
-		return "enter the %s word of the article's title" % n
-
-	def validate(self, form_data):
-		if form_data.captcha.lower() == self.answer.lower():
-			return True
-		return False
+
+class TitleCaptcha(object):
+    "Captcha that uses the article's title for the puzzle"
+
+    def __init__(self, article):
+        self.article = article
+        words = article.title.split()
+        self.nword = hash(article.title) % len(words) % 5
+        self.answer = words[self.nword]
+        self.help = "gotcha, damn spam bot!"
+
+    @property
+    def puzzle(self):
+        nword = self.nword + 1
+        if nword == 1:
+            n = "1st"
+        elif nword == 2:
+            n = "2nd"
+        elif nword == 3:
+            n = "3rd"
+        else:
+            n = str(nword) + "th"
+        return "enter the %s word of the article's title" % n
+
+    def validate(self, form_data):
+        if form_data.captcha.lower() == self.answer.lower():
+            return True
+        return False
+
 
 known_captcha_methods = {
-	'title': TitleCaptcha,
+    "title": TitleCaptcha,
 }
 
 # If the configured captcha method was a known string, replace it by the
@@ -182,7 +189,7 @@ known_captcha_methods = {
 # alone. This way the user can either use one of our methods, or provide one
 # of his/her own.
 if captcha_method in known_captcha_methods:
-	captcha_method = known_captcha_methods[captcha_method]
+    captcha_method = known_captcha_methods[captcha_method]
 
 
 # Default template
@@ -494,639 +501,649 @@ div.section h1 {
 # It only works if the function is pure (that is, its return value depends
 # only on its arguments), and if all the arguments are hash()eable.
 def cached(f):
-	# do not decorate if the cache is disabled
-	if cache_path is None:
-		return f
+    # do not decorate if the cache is disabled
+    if cache_path is None:
+        return f
 
-	def decorate(*args, **kwargs):
-		hashes = '-'.join( str(hash(x)) for x in args +
-				tuple(kwargs.items()) )
-		fname = 'blitiri.%s.%s.cache' % (f.__name__, hashes)
-		cache_file = os.path.join(cache_path, fname)
-		try:
-			s = open(cache_file).read()
-		except:
-			s = f(*args, **kwargs)
-			open(cache_file, 'w').write(s)
-		return s
+    def decorate(*args, **kwargs):
+        hashes = "-".join(str(hash(x)) for x in args + tuple(kwargs.items()))
+        fname = "blitiri.%s.%s.cache" % (f.__name__, hashes)
+        cache_file = os.path.join(cache_path, fname)
+        try:
+            s = open(cache_file).read()
+        except:
+            s = f(*args, **kwargs)
+            open(cache_file, "w").write(s)
+        return s
 
-	return decorate
+    return decorate
 
 
 # helper functions
 @cached
-def rst_to_html(rst, secure = True):
-	settings = {
-		'input_encoding': encoding,
-		'output_encoding': 'utf8',
-		'halt_level': 1,
-		'traceback':  1,
-		'file_insertion_enabled': secure,
-		'raw_enabled': secure,
-	}
-	parts = publish_parts(rst, settings_overrides = settings,
-				writer_name = "html")
-	return parts['body'].encode('utf8')
-
-def validate_rst(rst, secure = True):
-	try:
-		rst_to_html(rst, secure)
-		return None
-	except SystemMessage as e:
-		desc = e.args[0].encode('utf-8') # the error string
-		desc = desc[9:] # remove "<string>:"
-		line = int(desc[:desc.find(':')] or 0) # get the line number
-		desc = desc[desc.find(')')+2:-1] # remove (LEVEL/N)
-		try:
-			desc, context = desc.split('\n', 1)
-		except ValueError:
-			context = ''
-		if desc.endswith('.'):
-			desc = desc[:-1]
-		return (line, desc, context)
+def rst_to_html(rst, secure=True):
+    settings = {
+        "input_encoding": encoding,
+        "output_encoding": "utf8",
+        "halt_level": 1,
+        "traceback": 1,
+        "file_insertion_enabled": secure,
+        "raw_enabled": secure,
+    }
+    parts = publish_parts(rst, settings_overrides=settings, writer_name="html")
+    return parts["body"].encode("utf8")
+
+
+def validate_rst(rst, secure=True):
+    try:
+        rst_to_html(rst, secure)
+        return None
+    except SystemMessage as e:
+        desc = e.args[0].encode("utf-8")  # the error string
+        desc = desc[9:]  # remove "<string>:"
+        line = int(desc[: desc.find(":")] or 0)  # get the line number
+        desc = desc[desc.find(")") + 2 : -1]  # remove (LEVEL/N)
+        try:
+            desc, context = desc.split("\n", 1)
+        except ValueError:
+            context = ""
+        if desc.endswith("."):
+            desc = desc[:-1]
+        return (line, desc, context)
+
 
 def valid_link(link):
-	import re
-	scheme_re = r'^[a-zA-Z]+:'
-	mail_re = r"^[^ \t\n\r@<>()]+@[a-z0-9][a-z0-9\.\-_]*\.[a-z]+$"
-	url_re = r'^(?:[a-z0-9\-]+|[a-z0-9][a-z0-9\-\.\_]*\.[a-z]+)' \
-			r'(?::[0-9]+)?(?:/.*)?$'
-
-	if re.match(scheme_re, link, re.I):
-		scheme, rest = link.split(':', 1)
-		# if we have an scheme and a rest, assume the link is valid
-		# and return it as-is; otherwise (having just the scheme) is
-		# invalid
-		if rest:
-			return link
-		return None
-
-	# at this point, we don't have a scheme; we will try to recognize some
-	# common addresses (mail and http at the moment) and complete them to
-	# form a valid link, if we fail we will just claim it's invalid
-	if re.match(mail_re, link, re.I):
-		return 'mailto:' + link
-	elif re.match(url_re, link, re.I):
-		return 'https://' + link
-
-	return None
+    import re
+
+    scheme_re = r"^[a-zA-Z]+:"
+    mail_re = r"^[^ \t\n\r@<>()]+@[a-z0-9][a-z0-9\.\-_]*\.[a-z]+$"
+    url_re = (
+        r"^(?:[a-z0-9\-]+|[a-z0-9][a-z0-9\-\.\_]*\.[a-z]+)" r"(?::[0-9]+)?(?:/.*)?$"
+    )
+
+    if re.match(scheme_re, link, re.I):
+        scheme, rest = link.split(":", 1)
+        # if we have an scheme and a rest, assume the link is valid
+        # and return it as-is; otherwise (having just the scheme) is
+        # invalid
+        if rest:
+            return link
+        return None
+
+    # at this point, we don't have a scheme; we will try to recognize some
+    # common addresses (mail and http at the moment) and complete them to
+    # form a valid link, if we fail we will just claim it's invalid
+    if re.match(mail_re, link, re.I):
+        return "mailto:" + link
+    elif re.match(url_re, link, re.I):
+        return "https://" + link
+
+    return None
+
 
 def sanitize(obj):
-	return cgi.escape(obj, quote = True)
+    return cgi.escape(obj, quote=True)
 
 
 # find out our URL, needed for syndication
 try:
-	n = os.environ['SERVER_NAME']
-        # Removed port because it messes up when behind a proxy
-	#p = os.environ['SERVER_PORT']
-	s = os.environ['SCRIPT_NAME']
-	#if p == '80': p = ''
-	#else: p = ':' + p
-	full_url = 'https://%s%s' % (n, s)
+    n = os.environ["SERVER_NAME"]
+    # Removed port because it messes up when behind a proxy
+    # p = os.environ['SERVER_PORT']
+    s = os.environ["SCRIPT_NAME"]
+    # if p == '80': p = ''
+    # else: p = ':' + p
+    full_url = "https://%s%s" % (n, s)
 except KeyError:
-	full_url = 'Not needed'
-
-
-class Templates (object):
-	def __init__(self, tpath, db, showyear = None):
-		self.tpath = tpath
-		self.db = db
-		now = datetime.datetime.now()
-		if not showyear:
-			showyear = now.year
-
-		self.vars = {
-			'css_url': css_url,
-			'title': title,
-			'url': blog_url,
-			'fullurl': full_url,
-			'year': now.year,
-			'month': now.month,
-			'day': now.day,
-			'showyear': showyear,
-			'monthlinks': ' '.join(db.get_month_links(showyear)),
-			'yearlinks': ' '.join(db.get_year_links()),
-			'taglinks': ' '.join(db.get_tag_links()),
-		}
-
-	def get_template(self, page_name, default_template, extra_vars = None):
-		if extra_vars is None:
-			vars = self.vars
-		else:
-			vars = self.vars.copy()
-			vars.update(extra_vars)
-
-		p = '%s/%s.html' % (self.tpath, page_name)
-		if os.path.isfile(p):
-			return open(p).read() % vars
-		return default_template % vars
-
-	def get_main_header(self):
-		return self.get_template('header', default_main_header)
-
-	def get_main_footer(self):
-		return self.get_template('footer', default_main_footer)
-
-	def get_article_header(self, article):
-		return self.get_template(
-			'art_header', default_article_header, article.to_vars())
-
-	def get_article_footer(self, article):
-		return self.get_template(
-			'art_footer', default_article_footer, article.to_vars())
-
-	def get_comment_header(self, comment):
-		vars = comment.to_vars()
-		if comment.link:
-			vars['linked_author'] = '<a href="%s">%s</a>' \
-					% (vars['link'], vars['author'])
-		else:
-			vars['linked_author'] = vars['author']
-		return self.get_template(
-			'com_header', default_comment_header, vars)
-
-	def get_comment_footer(self, comment):
-		return self.get_template(
-			'com_footer', default_comment_footer, comment.to_vars())
-
-	def get_comment_form(self, article, form_data, captcha_puzzle):
-		vars = article.to_vars()
-		vars.update(form_data.to_vars(self))
-		vars['captcha_puzzle'] = captcha_puzzle
-		return self.get_template(
-			'com_form', default_comment_form, vars)
-
-	def get_comment_error(self, error):
-		return self.get_template(
-			'com_error', default_comment_error, dict(error=error))
-
-
-class CommentFormData (object):
-	def __init__(self, author = '', link = '', captcha = '', body = ''):
-		self.author = author
-		self.link = link
-		self.captcha = captcha
-		self.body = body
-		self.author_error = ''
-		self.link_error = ''
-		self.captcha_error = ''
-		self.body_error = ''
-		self.action = ''
-		self.method = 'post'
-
-	def to_vars(self, template):
-		render_error = template.get_comment_error
-		a_error = self.author_error and render_error(self.author_error)
-		l_error = self.link_error and render_error(self.link_error)
-		c_error = self.captcha_error \
-				and render_error(self.captcha_error)
-		b_error = self.body_error and render_error(self.body_error)
-		return {
-			'form_author': sanitize(self.author),
-			'form_link': sanitize(self.link),
-			'form_captcha': sanitize(self.captcha),
-			'form_body': sanitize(self.body),
-
-			'form_author_error': a_error,
-			'form_link_error': l_error,
-			'form_captcha_error': c_error,
-			'form_body_error': b_error,
-
-			'form_action': self.action,
-			'form_method': self.method,
-		}
-
-
-class Comment (object):
-	def __init__(self, article, number, created = None):
-		self.article = article
-		self.number = number
-		if created is None:
-			self.created = datetime.datetime.now()
-		else:
-			self.created = created
-
-		self.loaded = False
-
-		# loaded on demand
-		self._author = author
-		self._link = ''
-		self._raw_content = 'Removed comment'
-
-	@property
-	def author(self):
-		if not self.loaded:
-			self.load()
-		return self._author
-
-	@property
-	def link(self):
-		if not self.loaded:
-			self.load()
-		return self._link
-
-	@property
-	def raw_content(self):
-		if not self.loaded:
-			self.load()
-		return self._raw_content
-
-	def set(self, author, raw_content, link = '', created = None):
-		self.loaded = True
-		self._author = author
-		self._raw_content = raw_content
-		self._link = link
-		self.created = created or datetime.datetime.now()
-
-
-	def load(self):
-		filename = os.path.join(comments_path, self.article.uuid,
-					str(self.number))
-		try:
-			raw = open(filename).readlines()
-		except:
-			return
-
-		count = 0
-		for l in raw:
-			if ':' in l:
-				name, value = l.split(':', 1)
-				if name.lower() == 'author':
-					self._author = value.strip()
-				elif name.lower() == 'link':
-					self._link = value.strip()
-			elif l == '\n':
-				# end of header
-				break
-			count += 1
-		self._raw_content = ''.join(raw[count + 1:])
-		self.loaded = True
-
-	def save(self):
-		filename = os.path.join(comments_path, self.article.uuid,
-					str(self.number))
-		try:
-			f = open(filename, 'w')
-			f.write('Author: %s\n' % self.author)
-			f.write('Link: %s\n' % self.link)
-			f.write('\n')
-			f.write(self.raw_content)
-		except:
-			return
-
-
-	def to_html(self):
-		return rst_to_html(self.raw_content)
-
-	def to_vars(self):
-		return {
-			'number': self.number,
-			'author': sanitize(self.author),
-			'link': sanitize(self.link),
-			'date': self.created.isoformat(' '),
-			'created': self.created.isoformat(' '),
-
-			'year': self.created.year,
-			'month': self.created.month,
-			'day': self.created.day,
-			'hour': self.created.hour,
-			'minute': self.created.minute,
-			'second': self.created.second,
-		}
-
-class CommentDB (object):
-	def __init__(self, article):
-		self.path = os.path.join(comments_path, article.uuid)
-		# if comments were enabled after the article was added, we
-		# will need to create the directory
-		if not os.path.exists(self.path):
-			os.mkdir(self.path, 0o777)
-
-		self.comments = []
-		self.load(article)
-
-	def load(self, article):
-		try:
-			f = open(os.path.join(self.path, 'db'))
-		except:
-			return
-
-		for l in f:
-			# Each line has the following comma separated format:
-			# number, created (epoch)
-			# Empty lines are meaningful and represent removed
-			# comments (so we can preserve the comment number)
-			l = l.split(',')
-			try:
-				n = int(l[0])
-				d = datetime.datetime.fromtimestamp(float(l[1]))
-			except:
-				# Removed/invalid comment
-				self.comments.append(None)
-				continue
-			self.comments.append(Comment(article, n, d))
-
-	def save(self):
-		old_db = os.path.join(self.path, 'db')
-		new_db = os.path.join(self.path, 'db.tmp')
-		f = open(new_db, 'w')
-		for c in self.comments:
-			s = ''
-			if c is not None:
-				s = ''
-				s += str(c.number) + ', '
-				s += str(time.mktime(c.created.timetuple()))
-			s += '\n'
-			f.write(s)
-		f.close()
-		os.rename(new_db, old_db)
-
-
-class Article (object):
-	def __init__(self, path, created = None, updated = None):
-		self.path = path
-		self.created = created
-		self.updated = updated
-		self.uuid = "%08x" % zlib.crc32(self.path)
-
-		self.loaded = False
-
-		# loaded on demand
-		self._title = 'Removed post'
-		self._author = author
-		self._tags = []
-		self._raw_content = ''
-		self._comments = []
-
-	@property
-	def title(self):
-		if not self.loaded:
-			self.load()
-		return self._title
-
-	@property
-	def author(self):
-		if not self.loaded:
-			self.load()
-		return self._author
-
-	@property
-	def tags(self):
-		if not self.loaded:
-			self.load()
-		return self._tags
-
-	@property
-	def raw_content(self):
-		if not self.loaded:
-			self.load()
-		return self._raw_content
-
-	@property
-	def comments(self):
-		if not self.loaded:
-			self.load()
-		return self._comments
-
-
-	def __eq__(self, other):
-		if self.path == other.path:
-			return True
-		return False
-
-
-	def add_comment(self, author, raw_content, link = ''):
-		c = Comment(self, len(self.comments))
-		c.set(author, raw_content, link)
-		self.comments.append(c)
-		return c
-
-
-	def load(self):
-		# XXX this tweak is only needed for old DB format, where
-		# article's paths started with a slash
-		path = self.path
-		if path.startswith('/'):
-			path = path[1:]
-		filename = os.path.join(data_path, path)
-		try:
-			raw = open(filename).readlines()
-		except:
-			return
-
-		count = 0
-		for l in raw:
-			if ':' in l:
-				name, value = l.split(':', 1)
-				if name.lower() == 'title':
-					self._title = value.strip()
-				elif name.lower() == 'author':
-					self._author = value.strip()
-				elif name.lower() == 'tags':
-					ts = value.split(',')
-					ts = [t.strip() for t in ts]
-					self._tags = set(ts)
-			elif l == '\n':
-				# end of header
-				break
-			count += 1
-		self._raw_content = ''.join(raw[count + 1:])
-		db = CommentDB(self)
-		self._comments = db.comments
-		self.loaded = True
-
-	def to_html(self):
-		dirname = os.path.dirname
-		post_url = '/'.join([dirname(full_url), 'posts',
-				dirname(self.path)])
-		post_dir = '/'.join([data_path, dirname(self.path)])
-
-		rst = self.raw_content.replace('##POST_URL##', post_url)
-		rst = rst.replace('##POST_DIR##', post_dir)
-		for pattern, replacement in rst_regexp_replaces:
-			rst = re.sub(pattern, replacement, rst)
-		return rst_to_html(rst)
-
-	def to_vars(self):
-		return {
-			'arttitle': sanitize(self.title),
-			'author': sanitize(self.author),
-			'date': self.created.isoformat(' '),
-			'uuid': self.uuid,
-			'tags': self.get_tags_links(),
-			'comments': len(self.comments),
-
-			'created': self.created.isoformat(' '),
-			'ciso': self.created.isoformat(),
-			'cyear': self.created.year,
-			'cmonth': self.created.month,
-			'cday': self.created.day,
-			'chour': self.created.hour,
-			'cminute': self.created.minute,
-			'csecond': self.created.second,
-
-			'updated': self.updated.isoformat(' '),
-			'uiso': self.updated.isoformat(),
-			'uyear': self.updated.year,
-			'umonth': self.updated.month,
-			'uday': self.updated.day,
-			'uhour': self.updated.hour,
-			'uminute': self.updated.minute,
-			'usecond': self.updated.second,
-		}
-
-	def get_tags_links(self):
-		l = []
-		tags = sorted(self.tags)
-		for t in tags:
-			l.append('<a class="tag" href="%s/tag/%s">%s</a>' % \
-				(blog_url, urllib.parse.quote(t), sanitize(t) ))
-		return ', '.join(l)
-
-
-class ArticleDB (object):
-	def __init__(self, dbpath):
-		self.dbpath = dbpath
-		self.articles = []
-		self.uuids = {}
-		self.actyears = set()
-		self.actmonths = set()
-		self.acttags = set()
-		self.load()
-
-	def get_articles(self, year = 0, month = 0, day = 0, tags = None):
-		l = []
-		for a in self.articles:
-			if year and a.created.year != year: continue
-			if month and a.created.month != month: continue
-			if day and a.created.day != day: continue
-			if tags and not tags.issubset(a.tags): continue
-
-			l.append(a)
-
-		return l
-
-	def get_article(self, uuid):
-		return self.uuids[uuid]
-
-	def load(self):
-		try:
-			f = open(self.dbpath)
-		except:
-			return
-
-		for l in f:
-			# Each line has the following comma separated format:
-			# path (relative to data_path), \
-			#	created (epoch), \
-			# 	updated (epoch)
-			try:
-				l = l.split(',')
-			except:
-				continue
-
-			a = Article(l[0],
-				datetime.datetime.fromtimestamp(float(l[1])),
-				datetime.datetime.fromtimestamp(float(l[2])))
-			self.uuids[a.uuid] = a
-			self.acttags.update(a.tags)
-			self.actyears.add(a.created.year)
-			self.actmonths.add((a.created.year, a.created.month))
-			self.articles.append(a)
-
-	def save(self):
-		f = open(self.dbpath + '.tmp', 'w')
-		for a in self.articles:
-			s = ''
-			s += a.path + ', '
-			s += str(time.mktime(a.created.timetuple())) + ', '
-			s += str(time.mktime(a.updated.timetuple())) + '\n'
-			f.write(s)
-		f.close()
-		os.rename(self.dbpath + '.tmp', self.dbpath)
-
-	def get_year_links(self):
-		yl = list(self.actyears)
-		yl.sort(reverse = True)
-		return [ '<a href="%s/%d/">%d</a>' % (blog_url, y, y)
-				for y in yl ]
-
-	def get_month_links(self, year):
-		am = [ i[1] for i in self.actmonths if i[0] == year ]
-		ml = []
-		for i in range(1, 13):
-			name = calendar.month_name[i][:3]
-			if i in am:
-				s = '<a href="%s/%d/%d/">%s</a>' % \
-					( blog_url, year, i, name )
-			else:
-				s = name
-			ml.append(s)
-		return ml
-
-	def get_tag_links(self):
-		tl = sorted(self.acttags)
-		return [ '<a href="%s/tag/%s">%s</a>' % (blog_url,
-				sanitize(t), sanitize(t)) for t in tl ]
+    full_url = "Not needed"
+
+
+class Templates(object):
+    def __init__(self, tpath, db, showyear=None):
+        self.tpath = tpath
+        self.db = db
+        now = datetime.datetime.now()
+        if not showyear:
+            showyear = now.year
+
+        self.vars = {
+            "css_url": css_url,
+            "title": title,
+            "url": blog_url,
+            "fullurl": full_url,
+            "year": now.year,
+            "month": now.month,
+            "day": now.day,
+            "showyear": showyear,
+            "monthlinks": " ".join(db.get_month_links(showyear)),
+            "yearlinks": " ".join(db.get_year_links()),
+            "taglinks": " ".join(db.get_tag_links()),
+        }
+
+    def get_template(self, page_name, default_template, extra_vars=None):
+        if extra_vars is None:
+            vars = self.vars
+        else:
+            vars = self.vars.copy()
+            vars.update(extra_vars)
+
+        p = "%s/%s.html" % (self.tpath, page_name)
+        if os.path.isfile(p):
+            return open(p).read() % vars
+        return default_template % vars
+
+    def get_main_header(self):
+        return self.get_template("header", default_main_header)
+
+    def get_main_footer(self):
+        return self.get_template("footer", default_main_footer)
+
+    def get_article_header(self, article):
+        return self.get_template(
+            "art_header", default_article_header, article.to_vars()
+        )
+
+    def get_article_footer(self, article):
+        return self.get_template(
+            "art_footer", default_article_footer, article.to_vars()
+        )
+
+    def get_comment_header(self, comment):
+        vars = comment.to_vars()
+        if comment.link:
+            vars["linked_author"] = '<a href="%s">%s</a>' % (
+                vars["link"],
+                vars["author"],
+            )
+        else:
+            vars["linked_author"] = vars["author"]
+        return self.get_template("com_header", default_comment_header, vars)
+
+    def get_comment_footer(self, comment):
+        return self.get_template(
+            "com_footer", default_comment_footer, comment.to_vars()
+        )
+
+    def get_comment_form(self, article, form_data, captcha_puzzle):
+        vars = article.to_vars()
+        vars.update(form_data.to_vars(self))
+        vars["captcha_puzzle"] = captcha_puzzle
+        return self.get_template("com_form", default_comment_form, vars)
+
+    def get_comment_error(self, error):
+        return self.get_template("com_error", default_comment_error, dict(error=error))
+
+
+class CommentFormData(object):
+    def __init__(self, author="", link="", captcha="", body=""):
+        self.author = author
+        self.link = link
+        self.captcha = captcha
+        self.body = body
+        self.author_error = ""
+        self.link_error = ""
+        self.captcha_error = ""
+        self.body_error = ""
+        self.action = ""
+        self.method = "post"
+
+    def to_vars(self, template):
+        render_error = template.get_comment_error
+        a_error = self.author_error and render_error(self.author_error)
+        l_error = self.link_error and render_error(self.link_error)
+        c_error = self.captcha_error and render_error(self.captcha_error)
+        b_error = self.body_error and render_error(self.body_error)
+        return {
+            "form_author": sanitize(self.author),
+            "form_link": sanitize(self.link),
+            "form_captcha": sanitize(self.captcha),
+            "form_body": sanitize(self.body),
+            "form_author_error": a_error,
+            "form_link_error": l_error,
+            "form_captcha_error": c_error,
+            "form_body_error": b_error,
+            "form_action": self.action,
+            "form_method": self.method,
+        }
+
+
+class Comment(object):
+    def __init__(self, article, number, created=None):
+        self.article = article
+        self.number = number
+        if created is None:
+            self.created = datetime.datetime.now()
+        else:
+            self.created = created
+
+        self.loaded = False
+
+        # loaded on demand
+        self._author = author
+        self._link = ""
+        self._raw_content = "Removed comment"
+
+    @property
+    def author(self):
+        if not self.loaded:
+            self.load()
+        return self._author
+
+    @property
+    def link(self):
+        if not self.loaded:
+            self.load()
+        return self._link
+
+    @property
+    def raw_content(self):
+        if not self.loaded:
+            self.load()
+        return self._raw_content
+
+    def set(self, author, raw_content, link="", created=None):
+        self.loaded = True
+        self._author = author
+        self._raw_content = raw_content
+        self._link = link
+        self.created = created or datetime.datetime.now()
+
+    def load(self):
+        filename = os.path.join(comments_path, self.article.uuid, str(self.number))
+        try:
+            raw = open(filename).readlines()
+        except:
+            return
+
+        count = 0
+        for l in raw:
+            if ":" in l:
+                name, value = l.split(":", 1)
+                if name.lower() == "author":
+                    self._author = value.strip()
+                elif name.lower() == "link":
+                    self._link = value.strip()
+            elif l == "\n":
+                # end of header
+                break
+            count += 1
+        self._raw_content = "".join(raw[count + 1 :])
+        self.loaded = True
+
+    def save(self):
+        filename = os.path.join(comments_path, self.article.uuid, str(self.number))
+        try:
+            f = open(filename, "w")
+            f.write("Author: %s\n" % self.author)
+            f.write("Link: %s\n" % self.link)
+            f.write("\n")
+            f.write(self.raw_content)
+        except:
+            return
+
+    def to_html(self):
+        return rst_to_html(self.raw_content)
+
+    def to_vars(self):
+        return {
+            "number": self.number,
+            "author": sanitize(self.author),
+            "link": sanitize(self.link),
+            "date": self.created.isoformat(" "),
+            "created": self.created.isoformat(" "),
+            "year": self.created.year,
+            "month": self.created.month,
+            "day": self.created.day,
+            "hour": self.created.hour,
+            "minute": self.created.minute,
+            "second": self.created.second,
+        }
+
+
+class CommentDB(object):
+    def __init__(self, article):
+        self.path = os.path.join(comments_path, article.uuid)
+        # if comments were enabled after the article was added, we
+        # will need to create the directory
+        if not os.path.exists(self.path):
+            os.mkdir(self.path, 0o777)
+
+        self.comments = []
+        self.load(article)
+
+    def load(self, article):
+        try:
+            f = open(os.path.join(self.path, "db"))
+        except:
+            return
+
+        for l in f:
+            # Each line has the following comma separated format:
+            # number, created (epoch)
+            # Empty lines are meaningful and represent removed
+            # comments (so we can preserve the comment number)
+            l = l.split(",")
+            try:
+                n = int(l[0])
+                d = datetime.datetime.fromtimestamp(float(l[1]))
+            except:
+                # Removed/invalid comment
+                self.comments.append(None)
+                continue
+            self.comments.append(Comment(article, n, d))
+
+    def save(self):
+        old_db = os.path.join(self.path, "db")
+        new_db = os.path.join(self.path, "db.tmp")
+        f = open(new_db, "w")
+        for c in self.comments:
+            s = ""
+            if c is not None:
+                s = ""
+                s += str(c.number) + ", "
+                s += str(time.mktime(c.created.timetuple()))
+            s += "\n"
+            f.write(s)
+        f.close()
+        os.rename(new_db, old_db)
+
+
+class Article(object):
+    def __init__(self, path, created=None, updated=None):
+        self.path = path
+        self.created = created
+        self.updated = updated
+        self.uuid = "%08x" % zlib.crc32(self.path)
+
+        self.loaded = False
+
+        # loaded on demand
+        self._title = "Removed post"
+        self._author = author
+        self._tags = []
+        self._raw_content = ""
+        self._comments = []
+
+    @property
+    def title(self):
+        if not self.loaded:
+            self.load()
+        return self._title
+
+    @property
+    def author(self):
+        if not self.loaded:
+            self.load()
+        return self._author
+
+    @property
+    def tags(self):
+        if not self.loaded:
+            self.load()
+        return self._tags
+
+    @property
+    def raw_content(self):
+        if not self.loaded:
+            self.load()
+        return self._raw_content
+
+    @property
+    def comments(self):
+        if not self.loaded:
+            self.load()
+        return self._comments
+
+    def __eq__(self, other):
+        if self.path == other.path:
+            return True
+        return False
+
+    def add_comment(self, author, raw_content, link=""):
+        c = Comment(self, len(self.comments))
+        c.set(author, raw_content, link)
+        self.comments.append(c)
+        return c
+
+    def load(self):
+        # XXX this tweak is only needed for old DB format, where
+        # article's paths started with a slash
+        path = self.path
+        if path.startswith("/"):
+            path = path[1:]
+        filename = os.path.join(data_path, path)
+        try:
+            raw = open(filename).readlines()
+        except:
+            return
+
+        count = 0
+        for l in raw:
+            if ":" in l:
+                name, value = l.split(":", 1)
+                if name.lower() == "title":
+                    self._title = value.strip()
+                elif name.lower() == "author":
+                    self._author = value.strip()
+                elif name.lower() == "tags":
+                    ts = value.split(",")
+                    ts = [t.strip() for t in ts]
+                    self._tags = set(ts)
+            elif l == "\n":
+                # end of header
+                break
+            count += 1
+        self._raw_content = "".join(raw[count + 1 :])
+        db = CommentDB(self)
+        self._comments = db.comments
+        self.loaded = True
+
+    def to_html(self):
+        dirname = os.path.dirname
+        post_url = "/".join([dirname(full_url), "posts", dirname(self.path)])
+        post_dir = "/".join([data_path, dirname(self.path)])
+
+        rst = self.raw_content.replace("##POST_URL##", post_url)
+        rst = rst.replace("##POST_DIR##", post_dir)
+        for pattern, replacement in rst_regexp_replaces:
+            rst = re.sub(pattern, replacement, rst)
+        return rst_to_html(rst)
+
+    def to_vars(self):
+        return {
+            "arttitle": sanitize(self.title),
+            "author": sanitize(self.author),
+            "date": self.created.isoformat(" "),
+            "uuid": self.uuid,
+            "tags": self.get_tags_links(),
+            "comments": len(self.comments),
+            "created": self.created.isoformat(" "),
+            "ciso": self.created.isoformat(),
+            "cyear": self.created.year,
+            "cmonth": self.created.month,
+            "cday": self.created.day,
+            "chour": self.created.hour,
+            "cminute": self.created.minute,
+            "csecond": self.created.second,
+            "updated": self.updated.isoformat(" "),
+            "uiso": self.updated.isoformat(),
+            "uyear": self.updated.year,
+            "umonth": self.updated.month,
+            "uday": self.updated.day,
+            "uhour": self.updated.hour,
+            "uminute": self.updated.minute,
+            "usecond": self.updated.second,
+        }
+
+    def get_tags_links(self):
+        l = []
+        tags = sorted(self.tags)
+        for t in tags:
+            l.append(
+                '<a class="tag" href="%s/tag/%s">%s</a>'
+                % (blog_url, urllib.parse.quote(t), sanitize(t))
+            )
+        return ", ".join(l)
+
+
+class ArticleDB(object):
+    def __init__(self, dbpath):
+        self.dbpath = dbpath
+        self.articles = []
+        self.uuids = {}
+        self.actyears = set()
+        self.actmonths = set()
+        self.acttags = set()
+        self.load()
+
+    def get_articles(self, year=0, month=0, day=0, tags=None):
+        l = []
+        for a in self.articles:
+            if year and a.created.year != year:
+                continue
+            if month and a.created.month != month:
+                continue
+            if day and a.created.day != day:
+                continue
+            if tags and not tags.issubset(a.tags):
+                continue
+
+            l.append(a)
+
+        return l
+
+    def get_article(self, uuid):
+        return self.uuids[uuid]
+
+    def load(self):
+        try:
+            f = open(self.dbpath)
+        except:
+            return
+
+        for l in f:
+            # Each line has the following comma separated format:
+            # path (relative to data_path), \
+            # 	created (epoch), \
+            # 	updated (epoch)
+            try:
+                l = l.split(",")
+            except:
+                continue
+
+            a = Article(
+                l[0],
+                datetime.datetime.fromtimestamp(float(l[1])),
+                datetime.datetime.fromtimestamp(float(l[2])),
+            )
+            self.uuids[a.uuid] = a
+            self.acttags.update(a.tags)
+            self.actyears.add(a.created.year)
+            self.actmonths.add((a.created.year, a.created.month))
+            self.articles.append(a)
+
+    def save(self):
+        f = open(self.dbpath + ".tmp", "w")
+        for a in self.articles:
+            s = ""
+            s += a.path + ", "
+            s += str(time.mktime(a.created.timetuple())) + ", "
+            s += str(time.mktime(a.updated.timetuple())) + "\n"
+            f.write(s)
+        f.close()
+        os.rename(self.dbpath + ".tmp", self.dbpath)
+
+    def get_year_links(self):
+        yl = list(self.actyears)
+        yl.sort(reverse=True)
+        return ['<a href="%s/%d/">%d</a>' % (blog_url, y, y) for y in yl]
+
+    def get_month_links(self, year):
+        am = [i[1] for i in self.actmonths if i[0] == year]
+        ml = []
+        for i in range(1, 13):
+            name = calendar.month_name[i][:3]
+            if i in am:
+                s = '<a href="%s/%d/%d/">%s</a>' % (blog_url, year, i, name)
+            else:
+                s = name
+            ml.append(s)
+        return ml
+
+    def get_tag_links(self):
+        tl = sorted(self.acttags)
+        return [
+            '<a href="%s/tag/%s">%s</a>' % (blog_url, sanitize(t), sanitize(t))
+            for t in tl
+        ]
+
 
 #
 # Main
 #
 
+
 def render_comments(article, template, form_data):
-	print('<a name="comments" />')
-	for c in article.comments:
-		if c is None:
-			continue
-		print(template.get_comment_header(c))
-		print(c.to_html())
-		print(template.get_comment_footer(c))
-	if not form_data:
-		form_data = CommentFormData()
-	form_data.action = blog_url + '/comment/' + article.uuid + '#comment'
-	captcha = captcha_method(article)
-	print(template.get_comment_form(article, form_data, captcha.puzzle))
-
-def render_html(articles, db, actyear = None, show_comments = False,
-		redirect =  None, form_data = None):
-	if redirect:
-		print('Status: 303 See Other\r\n', end=' ')
-		print('Location: %s\r\n' % redirect, end=' ')
-	print('Content-type: text/html; charset=utf-8\r\n', end=' ')
-	print('\r\n', end=' ')
-	template = Templates(templates_path, db, actyear)
-	print(template.get_main_header())
-	for a in articles:
-		print(template.get_article_header(a))
-		print(a.to_html())
-		print(template.get_article_footer(a))
-		if show_comments:
-			render_comments(a, template, form_data)
-	print(template.get_main_footer())
-
-def render_artlist(articles, db, actyear = None):
-	template = Templates(templates_path, db, actyear)
-	print('Content-type: text/html; charset=utf-8\n')
-	print(template.get_main_header())
-	print('<h2>Articles</h2>')
-	for a in articles:
-		print('<li><a href="%(url)s/post/%(uuid)s">%(title)s</a></li>' \
-			% {	'url': blog_url,
-				'uuid': a.uuid,
-				'title': a.title,
-				'author': a.author,
-			})
-	print(template.get_main_footer())
+    print('<a name="comments" />')
+    for c in article.comments:
+        if c is None:
+            continue
+        print(template.get_comment_header(c))
+        print(c.to_html())
+        print(template.get_comment_footer(c))
+    if not form_data:
+        form_data = CommentFormData()
+    form_data.action = blog_url + "/comment/" + article.uuid + "#comment"
+    captcha = captcha_method(article)
+    print(template.get_comment_form(article, form_data, captcha.puzzle))
+
+
+def render_html(
+    articles, db, actyear=None, show_comments=False, redirect=None, form_data=None
+):
+    if redirect:
+        print("Status: 303 See Other\r\n", end=" ")
+        print("Location: %s\r\n" % redirect, end=" ")
+    print("Content-type: text/html; charset=utf-8\r\n", end=" ")
+    print("\r\n", end=" ")
+    template = Templates(templates_path, db, actyear)
+    print(template.get_main_header())
+    for a in articles:
+        print(template.get_article_header(a))
+        print(a.to_html())
+        print(template.get_article_footer(a))
+        if show_comments:
+            render_comments(a, template, form_data)
+    print(template.get_main_footer())
+
+
+def render_artlist(articles, db, actyear=None):
+    template = Templates(templates_path, db, actyear)
+    print("Content-type: text/html; charset=utf-8\n")
+    print(template.get_main_header())
+    print("<h2>Articles</h2>")
+    for a in articles:
+        print(
+            '<li><a href="%(url)s/post/%(uuid)s">%(title)s</a></li>'
+            % {
+                "url": blog_url,
+                "uuid": a.uuid,
+                "title": a.title,
+                "author": a.author,
+            }
+        )
+    print(template.get_main_footer())
+
 
 def render_atom(articles):
-	if len(articles) > 0:
-		updated = articles[0].updated.isoformat()
-	else:
-		updated = datetime.datetime.now().isoformat()
+    if len(articles) > 0:
+        updated = articles[0].updated.isoformat()
+    else:
+        updated = datetime.datetime.now().isoformat()
 
-	print('Content-type: application/atom+xml; charset=utf-8\n')
-	print("""<?xml version="1.0" encoding="utf-8"?>
+    print("Content-type: application/atom+xml; charset=utf-8\n")
+    print(
+        """<?xml version="1.0" encoding="utf-8"?>
 
 <feed xmlns="http://www.w3.org/2005/Atom">
  <title>%(title)s</title>
@@ -1135,19 +1152,24 @@ def render_atom(articles):
  <id>%(url)s</id> <!-- TODO: find a better <id>, see RFC 4151 -->
  <updated>%(updated)sZ</updated>
 
-	""" % {
-		'title': title,
-		'url': full_url,
-		'updated': updated,
-	})
-
-	for a in articles:
-		vars = a.to_vars()
-		vars.update( {
-			'url': full_url,
-			'contents': a.to_html(),
-		} )
-		print("""
+	"""
+        % {
+            "title": title,
+            "url": full_url,
+            "updated": updated,
+        }
+    )
+
+    for a in articles:
+        vars = a.to_vars()
+        vars.update(
+            {
+                "url": full_url,
+                "contents": a.to_html(),
+            }
+        )
+        print(
+            """
   <entry>
     <title>%(arttitle)s</title>
     <author><name>%(author)s</name></author>
@@ -1162,13 +1184,16 @@ def render_atom(articles):
       </div>
     </content>
   </entry>
-		""" % vars)
-	print("</feed>")
+		"""
+            % vars
+        )
+    print("</feed>")
 
 
 def render_style():
-	print('Content-type: text/css\r\n\r\n', end=' ')
-	print(default_css)
+    print("Content-type: text/css\r\n\r\n", end=" ")
+    print(default_css)
+
 
 # Get a dictionary with sort() arguments (key and reverse) by parsing the sort
 # specification format:
@@ -1180,241 +1205,249 @@ def render_style():
 # should be provided using the same format specification, with the difference
 # that all values must be provided for the default.
 def get_sort_args(sort_str, default):
-	def parse(s):
-		d = dict()
-		if not s:
-			return d
-		key = None
-		if len(s) > 0:
-			# accept ' ' as an alias of '+' since '+' is translated
-			# to ' ' in URLs
-			if s[0] in ('+', ' ', '-'):
-				key = s[1:]
-				d['reverse'] = (s[0] == '-')
-			else:
-				key = s
-		if key in ('title', 'author', 'created', 'updated', 'uuid'):
-			d['key'] = lambda a: getattr(a, key)
-		return d
-	args = parse(default)
-	assert args['key'] is not None and args['reverse'] is not None
-	args.update(parse(sort_str))
-	return args
+    def parse(s):
+        d = dict()
+        if not s:
+            return d
+        key = None
+        if len(s) > 0:
+            # accept ' ' as an alias of '+' since '+' is translated
+            # to ' ' in URLs
+            if s[0] in ("+", " ", "-"):
+                key = s[1:]
+                d["reverse"] = s[0] == "-"
+            else:
+                key = s
+        if key in ("title", "author", "created", "updated", "uuid"):
+            d["key"] = lambda a: getattr(a, key)
+        return d
+
+    args = parse(default)
+    assert args["key"] is not None and args["reverse"] is not None
+    args.update(parse(sort_str))
+    return args
+
 
 def handle_cgi():
-	import cgitb; cgitb.enable()
-
-	form = cgi.FieldStorage()
-	year = int(form.getfirst("year", 0))
-	month = int(form.getfirst("month", 0))
-	day = int(form.getfirst("day", 0))
-	tags = set(form.getlist("tag"))
-	sort_str = form.getfirst("sort", None)
-	uuid = None
-	atom = False
-	style = False
-	post = False
-	post_preview = False
-	artlist = False
-	comment = False
-
-	if 'PATH_INFO' in os.environ:
-		path_info = os.environ['PATH_INFO']
-		style = path_info == '/style'
-		atom = path_info == '/atom'
-		tag = path_info.startswith('/tag/')
-		post = path_info.startswith('/post/')
-		post_preview = path_info.startswith('/preview/post/')
-		artlist = path_info.startswith('/list')
-		comment = path_info.startswith('/comment/') and enable_comments
-		if not style and not atom and not post and not post_preview \
-				and not tag and not comment and not artlist:
-			date = path_info.split('/')[1:]
-			try:
-				if len(date) > 1 and date[0]:
-					year = int(date[0])
-				if len(date) > 2 and date[1]:
-					month = int(date[1])
-				if len(date) > 3 and date[2]:
-					day = int(date[2])
-			except ValueError:
-				pass
-		elif post:
-			uuid = path_info.replace('/post/', '')
-			uuid = uuid.replace('/', '')
-		elif post_preview:
-			art_path = path_info.replace('/preview/post/', '')
-			art_path = urllib.parse.unquote_plus(art_path)
-			art_path = os.path.join(data_path, art_path)
-			art_path = os.path.realpath(art_path)
-			common = os.path.commonprefix([data_path, art_path])
-			if common != data_path: # something nasty happened
-				post_preview = False
-			art_path = art_path[len(data_path)+1:]
-		elif tag:
-			t = path_info.replace('/tag/', '')
-			t = t.replace('/', '')
-			t = urllib.parse.unquote_plus(t)
-			tags = {t}
-		elif comment:
-			uuid = path_info.replace('/comment/', '')
-			uuid = uuid.replace('#comment', '')
-			uuid = uuid.replace('/', '')
-			author = form.getfirst('comformauthor', '')
-			link = form.getfirst('comformlink', '')
-			captcha = form.getfirst('comformcaptcha', '')
-			body = form.getfirst('comformbody', '')
-
-	db = ArticleDB(os.path.join(data_path, 'db'))
-	if atom:
-		articles = db.get_articles(tags = tags)
-		articles.sort(**get_sort_args(sort_str, '-created'))
-		render_atom(articles[:index_articles])
-	elif style:
-		render_style()
-	elif post:
-		render_html( [db.get_article(uuid)], db, year, enable_comments )
-	elif post_preview:
-		article = Article(art_path, datetime.datetime.now(),
-					datetime.datetime.now())
-		render_html( [article], db, year, enable_comments )
-	elif artlist:
-		articles = db.get_articles()
-		articles.sort(**get_sort_args(sort_str, '+title'))
-		render_artlist(articles, db)
-	elif comment and enable_comments:
-		form_data = CommentFormData(author.strip().replace('\n', ' '),
-				link.strip().replace('\n', ' '), captcha,
-				body.replace('\r', ''))
-		article = db.get_article(uuid)
-		captcha = captcha_method(article)
-		redirect = False
-		valid = True
-		if not form_data.author:
-			form_data.author_error = 'please, enter your name'
-			valid = False
-		if form_data.link:
-			link = valid_link(form_data.link)
-			if link:
-				form_data.link = link
-			else:
-				form_data.link_error = 'please, enter a ' \
-						'valid link'
-				valid = False
-		if not captcha.validate(form_data):
-			form_data.captcha_error = captcha.help
-			valid = False
-		if not form_data.body:
-			form_data.body_error = 'please, write a comment'
-			valid = False
-		else:
-			error = validate_rst(form_data.body, secure=False)
-			if error is not None:
-				(line, desc, ctx) = error
-				at = ''
-				if line:
-					at = ' at line %d' % line
-				form_data.body_error = 'error%s: %s' \
-						% (at, desc)
-				valid = False
-		if valid:
-			c = article.add_comment(form_data.author,
-					form_data.body, form_data.link)
-			c.save()
-			cdb = CommentDB(article)
-			cdb.comments = article.comments
-			cdb.save()
-			redirect = blog_url + '/post/' + uuid + '#comment-' \
-					+ str(c.number)
-		render_html( [article], db, year, enable_comments, redirect,
-				form_data )
-	else:
-		articles = db.get_articles(year, month, day, tags)
-		articles.sort(**get_sort_args(sort_str, '-created'))
-		if not year and not month and not day and not tags:
-			articles = articles[:index_articles]
-		render_html(articles, db, year)
+    import cgitb
+
+    cgitb.enable()
+
+    form = cgi.FieldStorage()
+    year = int(form.getfirst("year", 0))
+    month = int(form.getfirst("month", 0))
+    day = int(form.getfirst("day", 0))
+    tags = set(form.getlist("tag"))
+    sort_str = form.getfirst("sort", None)
+    uuid = None
+    atom = False
+    style = False
+    post = False
+    post_preview = False
+    artlist = False
+    comment = False
+
+    if "PATH_INFO" in os.environ:
+        path_info = os.environ["PATH_INFO"]
+        style = path_info == "/style"
+        atom = path_info == "/atom"
+        tag = path_info.startswith("/tag/")
+        post = path_info.startswith("/post/")
+        post_preview = path_info.startswith("/preview/post/")
+        artlist = path_info.startswith("/list")
+        comment = path_info.startswith("/comment/") and enable_comments
+        if (
+            not style
+            and not atom
+            and not post
+            and not post_preview
+            and not tag
+            and not comment
+            and not artlist
+        ):
+            date = path_info.split("/")[1:]
+            try:
+                if len(date) > 1 and date[0]:
+                    year = int(date[0])
+                if len(date) > 2 and date[1]:
+                    month = int(date[1])
+                if len(date) > 3 and date[2]:
+                    day = int(date[2])
+            except ValueError:
+                pass
+        elif post:
+            uuid = path_info.replace("/post/", "")
+            uuid = uuid.replace("/", "")
+        elif post_preview:
+            art_path = path_info.replace("/preview/post/", "")
+            art_path = urllib.parse.unquote_plus(art_path)
+            art_path = os.path.join(data_path, art_path)
+            art_path = os.path.realpath(art_path)
+            common = os.path.commonprefix([data_path, art_path])
+            if common != data_path:  # something nasty happened
+                post_preview = False
+            art_path = art_path[len(data_path) + 1 :]
+        elif tag:
+            t = path_info.replace("/tag/", "")
+            t = t.replace("/", "")
+            t = urllib.parse.unquote_plus(t)
+            tags = {t}
+        elif comment:
+            uuid = path_info.replace("/comment/", "")
+            uuid = uuid.replace("#comment", "")
+            uuid = uuid.replace("/", "")
+            author = form.getfirst("comformauthor", "")
+            link = form.getfirst("comformlink", "")
+            captcha = form.getfirst("comformcaptcha", "")
+            body = form.getfirst("comformbody", "")
+
+    db = ArticleDB(os.path.join(data_path, "db"))
+    if atom:
+        articles = db.get_articles(tags=tags)
+        articles.sort(**get_sort_args(sort_str, "-created"))
+        render_atom(articles[:index_articles])
+    elif style:
+        render_style()
+    elif post:
+        render_html([db.get_article(uuid)], db, year, enable_comments)
+    elif post_preview:
+        article = Article(art_path, datetime.datetime.now(), datetime.datetime.now())
+        render_html([article], db, year, enable_comments)
+    elif artlist:
+        articles = db.get_articles()
+        articles.sort(**get_sort_args(sort_str, "+title"))
+        render_artlist(articles, db)
+    elif comment and enable_comments:
+        form_data = CommentFormData(
+            author.strip().replace("\n", " "),
+            link.strip().replace("\n", " "),
+            captcha,
+            body.replace("\r", ""),
+        )
+        article = db.get_article(uuid)
+        captcha = captcha_method(article)
+        redirect = False
+        valid = True
+        if not form_data.author:
+            form_data.author_error = "please, enter your name"
+            valid = False
+        if form_data.link:
+            link = valid_link(form_data.link)
+            if link:
+                form_data.link = link
+            else:
+                form_data.link_error = "please, enter a " "valid link"
+                valid = False
+        if not captcha.validate(form_data):
+            form_data.captcha_error = captcha.help
+            valid = False
+        if not form_data.body:
+            form_data.body_error = "please, write a comment"
+            valid = False
+        else:
+            error = validate_rst(form_data.body, secure=False)
+            if error is not None:
+                (line, desc, ctx) = error
+                at = ""
+                if line:
+                    at = " at line %d" % line
+                form_data.body_error = "error%s: %s" % (at, desc)
+                valid = False
+        if valid:
+            c = article.add_comment(form_data.author, form_data.body, form_data.link)
+            c.save()
+            cdb = CommentDB(article)
+            cdb.comments = article.comments
+            cdb.save()
+            redirect = blog_url + "/post/" + uuid + "#comment-" + str(c.number)
+        render_html([article], db, year, enable_comments, redirect, form_data)
+    else:
+        articles = db.get_articles(year, month, day, tags)
+        articles.sort(**get_sort_args(sort_str, "-created"))
+        if not year and not month and not day and not tags:
+            articles = articles[:index_articles]
+        render_html(articles, db, year)
 
 
 def usage():
-	print('Usage: %s {add|rm|update} article_path' % sys.argv[0])
+    print("Usage: %s {add|rm|update} article_path" % sys.argv[0])
+
 
 def handle_cmd():
-	if len(sys.argv) != 3:
-		usage()
-		return 1
-
-	cmd = sys.argv[1]
-	art_path = os.path.realpath(sys.argv[2])
-
-	if os.path.commonprefix([data_path, art_path]) != data_path:
-		print("Error: article (%s) must be inside data_path (%s)" % \
-				(art_path, data_path))
-		return 1
-	art_path = art_path[len(data_path)+1:]
-
-	db_filename = os.path.join(data_path, 'db')
-	if not os.path.isfile(db_filename):
-		open(db_filename, 'w').write('')
-	db = ArticleDB(db_filename)
-
-	if cmd == 'add':
-		article = Article(art_path, datetime.datetime.now(),
-					datetime.datetime.now())
-		for a in db.articles:
-			if a == article:
-				print('Error: article already exists')
-				return 1
-		db.articles.append(article)
-		db.save()
-		if enable_comments:
-			comment_dir = os.path.join(comments_path, article.uuid)
-			try:
-				os.mkdir(comment_dir, 0o775)
-			except OSError as e:
-				if e.errno != errno.EEXIST:
-					print("Error: can't create comments " \
-						"directory %s (%s)" \
-							% (comment_dir, e))
-				# otherwise is probably a removed and re-added
-				# article
-	elif cmd == 'rm':
-		article = Article(art_path)
-		for a in db.articles:
-			if a == article:
-				break
-		else:
-			print("Error: no such article")
-			return 1
-		if enable_comments:
-			r = input('Remove comments [y/N]? ')
-		db.articles.remove(a)
-		db.save()
-		if enable_comments and r.lower() == 'y':
-			shutil.rmtree(os.path.join(comments_path, a.uuid))
-	elif cmd == 'update':
-		article = Article(art_path)
-		for a in db.articles:
-			if a == article:
-				break
-		else:
-			print("Error: no such article")
-			return 1
-		a.updated = datetime.datetime.now()
-		db.save()
-	else:
-		usage()
-		return 1
-
-	return 0
-
-
-if 'GATEWAY_INTERFACE' in os.environ:
-	i = datetime.datetime.now()
-	handle_cgi()
-	f = datetime.datetime.now()
-	print('<!-- render time: %s -->' % (f-i))
+    if len(sys.argv) != 3:
+        usage()
+        return 1
+
+    cmd = sys.argv[1]
+    art_path = os.path.realpath(sys.argv[2])
+
+    if os.path.commonprefix([data_path, art_path]) != data_path:
+        print(
+            "Error: article (%s) must be inside data_path (%s)" % (art_path, data_path)
+        )
+        return 1
+    art_path = art_path[len(data_path) + 1 :]
+
+    db_filename = os.path.join(data_path, "db")
+    if not os.path.isfile(db_filename):
+        open(db_filename, "w").write("")
+    db = ArticleDB(db_filename)
+
+    if cmd == "add":
+        article = Article(art_path, datetime.datetime.now(), datetime.datetime.now())
+        for a in db.articles:
+            if a == article:
+                print("Error: article already exists")
+                return 1
+        db.articles.append(article)
+        db.save()
+        if enable_comments:
+            comment_dir = os.path.join(comments_path, article.uuid)
+            try:
+                os.mkdir(comment_dir, 0o775)
+            except OSError as e:
+                if e.errno != errno.EEXIST:
+                    print(
+                        "Error: can't create comments "
+                        "directory %s (%s)" % (comment_dir, e)
+                    )
+                # otherwise is probably a removed and re-added
+                # article
+    elif cmd == "rm":
+        article = Article(art_path)
+        for a in db.articles:
+            if a == article:
+                break
+        else:
+            print("Error: no such article")
+            return 1
+        if enable_comments:
+            r = input("Remove comments [y/N]? ")
+        db.articles.remove(a)
+        db.save()
+        if enable_comments and r.lower() == "y":
+            shutil.rmtree(os.path.join(comments_path, a.uuid))
+    elif cmd == "update":
+        article = Article(art_path)
+        for a in db.articles:
+            if a == article:
+                break
+        else:
+            print("Error: no such article")
+            return 1
+        a.updated = datetime.datetime.now()
+        db.save()
+    else:
+        usage()
+        return 1
+
+    return 0
+
+
+if "GATEWAY_INTERFACE" in os.environ:
+    i = datetime.datetime.now()
+    handle_cgi()
+    f = datetime.datetime.now()
+    print("<!-- render time: %s -->" % (f - i))
 else:
-	sys.exit(handle_cmd())
-
-
+    sys.exit(handle_cmd())