From 5ca018a916a23bdfadbc2f063e7e82de885f04ed Mon Sep 17 00:00:00 2001 From: Mike Crute Date: Sun, 6 Jun 2010 21:46:53 -0400 Subject: Initial import. Really rough code. --- .hgignore | 2 + Makefile | 11 +++ metabuild.py | 23 ++++++ site_builder/__init__.py | 63 +++++++++++++++ site_builder/blog.py | 182 ++++++++++++++++++++++++++++++++++++++++++++ site_builder/blogbuilder.py | 88 +++++++++++++++++++++ site_builder/feeds.py | 111 +++++++++++++++++++++++++++ site_builder/pagebuilder.py | 2 + site_builder/robots.py | 15 ++++ site_builder/sitemap.py | 21 +++++ templates/blog_archive.html | 14 ++++ templates/blog_index.html | 18 +++++ templates/blog_post.html | 26 +++++++ templates/blog_tags.html | 22 ++++++ templates/page.html | 57 ++++++++++++++ 15 files changed, 655 insertions(+) create mode 100644 .hgignore create mode 100644 Makefile create mode 100755 metabuild.py create mode 100644 site_builder/__init__.py create mode 100644 site_builder/blog.py create mode 100644 site_builder/blogbuilder.py create mode 100644 site_builder/feeds.py create mode 100644 site_builder/pagebuilder.py create mode 100644 site_builder/robots.py create mode 100644 site_builder/sitemap.py create mode 100644 templates/blog_archive.html create mode 100644 templates/blog_index.html create mode 100644 templates/blog_post.html create mode 100644 templates/blog_tags.html create mode 100644 templates/page.html diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..a26d142 --- /dev/null +++ b/.hgignore @@ -0,0 +1,2 @@ +syntax:glob +*.pyc diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cb60495 --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +WEB_ROOT=/srv/www/crute.org/mike/htdocs + +site: + ./metabuild.py + rsync -auvz rendered/ $(WEB_ROOT) + chgrp -R www-data $(WEB_ROOT) + chmod -R g+r $(WEB_ROOT) + find $(WEB_ROOT) -type d -exec chmod g+x {} \; + +css: + python page_source/downloads/cmpcss page_source/resources/site.css > page_source/resources/site-min.css diff --git a/metabuild.py b/metabuild.py new file mode 100755 index 0000000..6508cbb --- /dev/null +++ b/metabuild.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +# vim: set filencoding=utf8 +""" +Website Build Script + +@author: Mike Crute (mcrute@ag.com) +@organization: American Greetings Interactive +@date: June 03, 2010 + +TODO: + * Site Map Builder + * Full Blog Builder + * Index page + * Archive page + * Feeds +""" + +if __name__ == '__main__': + from site_builder import build_all + from site_builder.blogbuilder import build_blog + build_all('page_source', 'rendered') + build_blog('page_source/blog', 'rendered/blog') + build_blog('page_source/personal_blog', 'rendered/personal_blog') diff --git a/site_builder/__init__.py b/site_builder/__init__.py new file mode 100644 index 0000000..f60820a --- /dev/null +++ b/site_builder/__init__.py @@ -0,0 +1,63 @@ +# vim: set filencoding=utf8 +""" +Site Builder + +@author: Mike Crute (mcrute@ag.com) +@organization: American Greetings Interactive +@date: June 04, 2010 +""" + +import os +import jinja2 +import pagebuilder +from datetime import datetime +from docutils.core import publish_parts +from docutils.io import FileInput + + +def get_template(name): + loader = jinja2.FileSystemLoader('templates') + renderer = jinja2.Environment(loader=loader) + return renderer.get_template(name) + + +def build_standard_page(filename, output_name): + parts = publish_parts(open(filename, 'r').read(), writer_name='html') + template = get_template('page.html') + + try: + os.makedirs(os.path.dirname(output_name)) + except OSError: + pass # directory exists + + open(output_name, 'w').write(template.render( + contents=parts['html_body'], + build_date=datetime.now().strftime('%B %d, %Y'), + source_link=filename)) + + +def get_output_name(base_dir, output_dir, filename): + base_depth = len(base_dir.split(os.path.sep)) + out_name = filename.split(os.path.sep)[base_depth:] + new_path = os.path.join(output_dir, *out_name) + + if new_path.endswith('.rst'): + new_path = new_path[:-len('.rst')] + '.html' + + return new_path + + +def build_all(base_dir, output_dir): + for root, dirs, files in os.walk(base_dir): + for filename in files: + if ('personal_blog' in root or + 'blog' in root or + not filename.endswith('.rst')): + continue + + old_path = os.path.join(root, filename) + new_path = get_output_name(base_dir, output_dir, old_path) + + print "BUILDING: ", old_path + + build_standard_page(old_path, new_path) diff --git a/site_builder/blog.py b/site_builder/blog.py new file mode 100644 index 0000000..afa1fc8 --- /dev/null +++ b/site_builder/blog.py @@ -0,0 +1,182 @@ +# vim: set filencoding=utf8 +""" +Blog Post Builder + +@author: Mike Crute (mcrute@ag.com) +@organization: American Greetings Interactive +@date: June 03, 2010 +""" + +import os +from functools import wraps +from datetime import datetime + +# Docutils imports, crazy yo +from docutils import nodes +from docutils.core import Publisher, publish_string +from docutils.transforms import Transform +from docutils.io import NullOutput, FileInput +from docutils.parsers.rst import Parser as RSTParser +from docutils.writers.html4css1 import Writer as HTMLWriter +from docutils.readers.standalone import Reader as StandaloneReader + + +class BlogMetaTransform(Transform): + """ + Removes metadata tags from the document tree. + + This transformer removes the metadata nodes from the document tree + and places them in a blog_meta dictionary on the document object. + This happens before rendering so the meta won't show up in the output. + """ + + default_priority = 360 # Fuck if I know, same as the PEP header transform + + def __init__(self, *args, **kwargs): + Transform.__init__(self, *args, **kwargs) + + self.meta = self.document.blog_meta = { + 'tags': [], + } + + def apply(self): + docinfo = None + + # One to get the docinfo and title + # We need a copy of the document as a list so we can modify it + # without messing up iteration. + for node in list(self.document): + if isinstance(node, nodes.docinfo): + docinfo = node + self.document.remove(node) + + if isinstance(node, nodes.title): + self.meta['title'] = unicode(node[0]) + self.document.remove(node) + + # And one to process the docinfo + for node in docinfo: + if isinstance(node, nodes.author): + self._handle_author(node) + + if isinstance(node, nodes.date): + self._handle_date(node) + + if isinstance(node, nodes.field): + self._handle_field(node) + + def _handle_author(self, node): + self.meta['author'] = Author(node[0]['name'], node[0]['refuri']) + + def _handle_date(self, node): + raw_date = unicode(node[0]) + self.meta['post_date'] = datetime.strptime(raw_date, + '%a %b %d %H:%M:%S %Y') + + def _handle_field(self, node): + name = node[0][0] + value = unicode(node[1][0][0]) + + if name == 'Tag': + self.meta['tags'].append(value) + + + +class BlogPostReader(StandaloneReader): + """ + Post reader for blog posts. + + This exists only so that we can append our custom blog + transformers on to the regular ones. + """ + + def get_transforms(self): + return StandaloneReader.get_transforms(self) + [ + BlogMetaTransform, + ] + + +class Author(object): + """ + Representation of the author information for a blog post. + """ + + def __init__(self, name, email): + self.name = name + self.email = email + + if email.startswith('mailto:'): + self.email = email[len('mailto:'):] + + def __str__(self): + return '{0} <{1}>'.format(self.name, self.email) + + +class BlogPost(object): + """ + Representation of a blog post. + + Constructed from a docutils dom version of the blog post. + """ + + def __init__(self, title, post_date, author, tags, contents=None): + self.title = title + self.post_date = post_date + self.author = author + self.tags = tags + self.contents = contents + self._filename = None + + @property + def filename(self): + return os.path.basename(self._filename) + + @filename.setter + def filename(self, value): + self._filename = value + + @property + def pretty_date(self): + return self.post_date.strftime("%B %d, %Y") + + @classmethod + def from_file(cls, filename): + """ + Loads a file from disk, parses it and constructs a new BlogPost. + + This method reflects a bit of the insanity of docutils. Basically + this is just the docutils.core.publish_doctree function with some + modifications to use an html writer and to load a file instead of + a string. + """ + pub = Publisher(destination_class=NullOutput, + source=FileInput(source_path=filename), + reader=BlogPostReader(), writer=HTMLWriter(), + parser=RSTParser()) + + pub.get_settings() # This is not sane. + pub.settings.traceback = True # Damnit + pub.publish() + + meta = pub.document.blog_meta + post = cls(meta['title'], meta['post_date'], meta['author'], + meta['tags'], pub.writer.parts['html_body']) + + post.filename = filename + + return post + + +def load_post_index(directory='.'): + """ + Scan the current directory for rst files and build an index. + """ + posts = [] + for filename in os.listdir(directory): + if not filename.endswith('.rst'): + continue + + filename = os.path.join(directory, filename) + posts.append(BlogPost.from_file(filename)) + + return posts diff --git a/site_builder/blogbuilder.py b/site_builder/blogbuilder.py new file mode 100644 index 0000000..ebe77c0 --- /dev/null +++ b/site_builder/blogbuilder.py @@ -0,0 +1,88 @@ +# vim: set filencoding=utf8 +""" +Blog Builder + +@author: Mike Crute (mcrute@ag.com) +@organization: American Greetings Interactive +@date: June 04, 2010 +""" + +import os +import operator +from datetime import datetime +from collections import defaultdict +from site_builder import get_template, get_output_name +from blog import load_post_index +from feeds import Atom1Feed + + +def build_feed(output_dir, post_index): + page_name = os.path.join(output_dir, 'feed.atom') + feed = Atom1Feed(post_index, "The Random Thoughts of a Programmer", + "http://mike.crute.org/blog/feed", + post_index[0].post_date, + "http://mike.crute.org/blog") + + open(page_name, 'w').write(feed.get_feed()) + + +def build_tags(output_dir, post_index): + tag_index = defaultdict(list) + + template = get_template('blog_tags.html') + page_name = os.path.join(output_dir, 'tags.html') + + for post in post_index: + for tag in post.tags: + tag_index[tag].append(post) + + tag_index = sorted(tag_index.items()) + open(page_name, 'w').write(template.render(posts=tag_index, + build_date=datetime.now().strftime("%B %d, %Y"))) + + +def build_archive(output_dir, post_index): + date_index = defaultdict(list) + + template = get_template('blog_archive.html') + page_name = os.path.join(output_dir, 'archive.html') + + for post in post_index: + date_index[post.post_date.year].append(post) + + date_index = sorted(date_index.items(), reverse=True) + open(page_name, 'w').write(template.render(posts=date_index, + build_date=datetime.now().strftime("%B %d, %Y"))) + + +def build_index(output_dir, post_index): + template = get_template('blog_index.html') + page_name = os.path.join(output_dir, 'index.html') + + open(page_name, 'w').write(template.render(posts=post_index[:3], + build_date=datetime.now().strftime("%B %d, %Y"))) + + +def build_blog(base_dir, output_dir): + post_index = load_post_index(base_dir) + post_index.sort(key=operator.attrgetter('post_date'), reverse=True) + + try: + os.makedirs(output_dir) + except OSError: + pass # directory already exists + + for post in post_index: + template = get_template('blog_post.html') + + out_filename = os.path.join(output_dir, post.filename) + out_filename = out_filename[:-len('rst')] + 'html' + + print "BUILDING BLOG: ", out_filename + + open(out_filename, 'w').write(template.render(post=post)) + + build_index(output_dir, post_index) + build_archive(output_dir, post_index) + build_feed(output_dir, post_index) + build_tags(output_dir, post_index) diff --git a/site_builder/feeds.py b/site_builder/feeds.py new file mode 100644 index 0000000..6c38443 --- /dev/null +++ b/site_builder/feeds.py @@ -0,0 +1,111 @@ +# vim: set filencoding=utf8 +""" +Atom Feed Writer + +@author: Mike Crute (mcrute@ag.com) +@organization: American Greetings Interactive +@date: June 04, 2010 +""" + +#from io import StringIO +from StringIO import StringIO +from xml.sax.saxutils import XMLGenerator + + +class SimpleXMLGenerator(XMLGenerator): + + def __init__(self, encoding='utf-8'): + self.output = StringIO() + XMLGenerator.__init__(self, out=self.output, encoding=encoding) + + def get_contents(self): + return self.output.getvalue() + + def startElement(self, tag, attrs=None): + attrs = attrs if attrs else {} + return XMLGenerator.startElement(self, tag, attrs) + + def addElement(self, tag, contents=None, attrs=None): + attrs = attrs if attrs else {} + self.startElement(tag, attrs) + if contents: + self.characters(contents) + self.endElement(tag) + + +class Atom1Feed(object): + + def __init__(self, posts, title, feed_url, updated, blog_url, + post_filter=None): + self.posts = posts + self.title = title + self.feed_url = feed_url + self.updated = updated + self.blog_url = blog_url + self.handler = SimpleXMLGenerator() + + if not post_filter: + post_filter = lambda post: True + + self.post_filter = post_filter + + def _format_time(self, timeobj): + return timeobj.strftime("%Y-%m-%dT%H:%M:%SZ") + + def get_feed(self): + self.handler.startDocument() + self.handler.startElement('feed', { + 'xmlns': 'http://www.w3.org/2005/Atom' }) + + self.add_root_elements() + + for post in self.posts: + if not self.post_filter(post): + continue + + self.add_post(post) + + self.handler.endElement('feed') + + return self.handler.get_contents() + + def add_root_elements(self): + self.handler.addElement('title', self.title) + self.handler.addElement('updated', self._format_time(self.updated)) + self.handler.addElement('id', self.feed_url) + self.handler.addElement('link', attrs={ + 'rel': 'alternate', + 'type': 'text/html', + 'href': self.blog_url }) + self.handler.addElement('link', attrs={ + 'rel': 'self', + 'type': 'application/atom+xml', + 'href': self.feed_url }) + + def add_post(self, post): + handler = self.handler + + handler.startElement('entry') + + handler.startElement('author') + handler.addElement('name', post.author.name) + handler.addElement('email', post.author.email) + handler.endElement('author') + + post_href = '{0}/{1}'.format(self.blog_url, post.filename) + + handler.addElement('title', post.title) + handler.addElement('link', attrs={ + 'rel': 'alternate', + 'type': 'text/html', + 'href': post_href }) + handler.addElement('id', post_href) + handler.addElement('updated', self._format_time(post.post_date)) + handler.addElement('published', self._format_time(post.post_date)) + + for tag in post.tags: + handler.addElement('category', attrs={ 'term': tag }) + + handler.addElement('content', post.contents, attrs={ 'type': 'html' }) + + handler.endElement('entry') diff --git a/site_builder/pagebuilder.py b/site_builder/pagebuilder.py new file mode 100644 index 0000000..2af7556 --- /dev/null +++ b/site_builder/pagebuilder.py @@ -0,0 +1,2 @@ +def build(): + pass diff --git a/site_builder/robots.py b/site_builder/robots.py new file mode 100644 index 0000000..9ad5698 --- /dev/null +++ b/site_builder/robots.py @@ -0,0 +1,15 @@ +# vim: set filencoding=utf8 +""" +Robots.txt Generator + +@author: Mike Crute (mcrute@ag.com) +@organization: American Greetings Interactive +@date: June 05, 2010 +""" + +""" +Sitemap: http://mike.crute.org/sitemap.xml + +User-agent: * +Disallow: /blog/wp-admin/ +""" diff --git a/site_builder/sitemap.py b/site_builder/sitemap.py new file mode 100644 index 0000000..b70bd24 --- /dev/null +++ b/site_builder/sitemap.py @@ -0,0 +1,21 @@ +# vim: set filencoding=utf8 +""" +Sitemap Generator + +@author: Mike Crute (mcrute@ag.com) +@organization: American Greetings Interactive +@date: June 05, 2010 +""" + +""" + + + + + http://mike.crute.org/blog/ + 2009-09-09T13:28:46+00:00 + daily + 1.0 + + +""" diff --git a/templates/blog_archive.html b/templates/blog_archive.html new file mode 100644 index 0000000..df6b810 --- /dev/null +++ b/templates/blog_archive.html @@ -0,0 +1,14 @@ +{% extends "page.html" %} + +{% set is_blog = True %} + +{% block contents %} +{% for year, post_set in posts %} +

{{ year }}

+ +{% endfor %} +{% endblock %} diff --git a/templates/blog_index.html b/templates/blog_index.html new file mode 100644 index 0000000..bb918fa --- /dev/null +++ b/templates/blog_index.html @@ -0,0 +1,18 @@ +{% extends "page.html" %} + +{% set is_blog = True %} + +{% block contents %} +{% for post in posts %} +

{{ post.title }}

+

Posted: {{ post.pretty_date }}

+{{ post.contents }} +{% if not loop.last %} +
+{% endif %} +{% endfor %} +{% endblock %} + +{% block update_date %} +

Page last updated: {{ posts[0].pretty_date }}

+{% endblock %} diff --git a/templates/blog_post.html b/templates/blog_post.html new file mode 100644 index 0000000..96c7bd9 --- /dev/null +++ b/templates/blog_post.html @@ -0,0 +1,26 @@ +{% extends "page.html" %} + +{% set is_blog = True %} + +{% block contents %} +

{{ post.title }}

+{{ post.contents }} + +{# Maybe enable comments... some day... +
+

Comments

+
+
+ +#} +{% endblock %} + +{% block update_date %} +

Page last updated: {{ post.pretty_date }}

+{% endblock %} diff --git a/templates/blog_tags.html b/templates/blog_tags.html new file mode 100644 index 0000000..bcf7d42 --- /dev/null +++ b/templates/blog_tags.html @@ -0,0 +1,22 @@ +{% extends "page.html" %} + +{% set is_blog = True %} + +{% block contents %} +

All Tags

+

+{%- for tag, post_set in posts %} +{{ tag }} +{% endfor %} +

+ + +{% for tag, post_set in posts %} +

{{ tag }}

+ +{% endfor %} +{% endblock %} diff --git a/templates/page.html b/templates/page.html new file mode 100644 index 0000000..83fa0dc --- /dev/null +++ b/templates/page.html @@ -0,0 +1,57 @@ + + + + + + + + +The Random Thoughts of a Programmer + + + + + + +
+ +{% block contents %} +{{ contents }} +{% endblock %} + +
+ + + + + -- cgit v1.2.3