summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mcrute@gmail.com>2010-06-06 21:46:53 -0400
committerMike Crute <mcrute@gmail.com>2010-06-06 21:46:53 -0400
commit5ca018a916a23bdfadbc2f063e7e82de885f04ed (patch)
tree82340b4f9f09d27628c027aa9547cea40f48dcd3
downloadventriloquy-5ca018a916a23bdfadbc2f063e7e82de885f04ed.tar.bz2
ventriloquy-5ca018a916a23bdfadbc2f063e7e82de885f04ed.tar.xz
ventriloquy-5ca018a916a23bdfadbc2f063e7e82de885f04ed.zip
Initial import. Really rough code.
-rw-r--r--.hgignore2
-rw-r--r--Makefile11
-rwxr-xr-xmetabuild.py23
-rw-r--r--site_builder/__init__.py63
-rw-r--r--site_builder/blog.py182
-rw-r--r--site_builder/blogbuilder.py88
-rw-r--r--site_builder/feeds.py111
-rw-r--r--site_builder/pagebuilder.py2
-rw-r--r--site_builder/robots.py15
-rw-r--r--site_builder/sitemap.py21
-rw-r--r--templates/blog_archive.html14
-rw-r--r--templates/blog_index.html18
-rw-r--r--templates/blog_post.html26
-rw-r--r--templates/blog_tags.html22
-rw-r--r--templates/page.html57
15 files changed, 655 insertions, 0 deletions
diff --git a/.hgignore b/.hgignore
new file mode 100644
index 0000000..a26d142
--- /dev/null
+++ b/.hgignore
@@ -0,0 +1,2 @@
1syntax:glob
2*.pyc
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..cb60495
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,11 @@
1WEB_ROOT=/srv/www/crute.org/mike/htdocs
2
3site:
4 ./metabuild.py
5 rsync -auvz rendered/ $(WEB_ROOT)
6 chgrp -R www-data $(WEB_ROOT)
7 chmod -R g+r $(WEB_ROOT)
8 find $(WEB_ROOT) -type d -exec chmod g+x {} \;
9
10css:
11 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 @@
1#!/usr/bin/env python
2# vim: set filencoding=utf8
3"""
4Website Build Script
5
6@author: Mike Crute (mcrute@ag.com)
7@organization: American Greetings Interactive
8@date: June 03, 2010
9
10TODO:
11 * Site Map Builder
12 * Full Blog Builder
13 * Index page
14 * Archive page
15 * Feeds
16"""
17
18if __name__ == '__main__':
19 from site_builder import build_all
20 from site_builder.blogbuilder import build_blog
21 build_all('page_source', 'rendered')
22 build_blog('page_source/blog', 'rendered/blog')
23 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 @@
1# vim: set filencoding=utf8
2"""
3Site Builder
4
5@author: Mike Crute (mcrute@ag.com)
6@organization: American Greetings Interactive
7@date: June 04, 2010
8"""
9
10import os
11import jinja2
12import pagebuilder
13from datetime import datetime
14from docutils.core import publish_parts
15from docutils.io import FileInput
16
17
18def get_template(name):
19 loader = jinja2.FileSystemLoader('templates')
20 renderer = jinja2.Environment(loader=loader)
21 return renderer.get_template(name)
22
23
24def build_standard_page(filename, output_name):
25 parts = publish_parts(open(filename, 'r').read(), writer_name='html')
26 template = get_template('page.html')
27
28 try:
29 os.makedirs(os.path.dirname(output_name))
30 except OSError:
31 pass # directory exists
32
33 open(output_name, 'w').write(template.render(
34 contents=parts['html_body'],
35 build_date=datetime.now().strftime('%B %d, %Y'),
36 source_link=filename))
37
38
39def get_output_name(base_dir, output_dir, filename):
40 base_depth = len(base_dir.split(os.path.sep))
41 out_name = filename.split(os.path.sep)[base_depth:]
42 new_path = os.path.join(output_dir, *out_name)
43
44 if new_path.endswith('.rst'):
45 new_path = new_path[:-len('.rst')] + '.html'
46
47 return new_path
48
49
50def build_all(base_dir, output_dir):
51 for root, dirs, files in os.walk(base_dir):
52 for filename in files:
53 if ('personal_blog' in root or
54 'blog' in root or
55 not filename.endswith('.rst')):
56 continue
57
58 old_path = os.path.join(root, filename)
59 new_path = get_output_name(base_dir, output_dir, old_path)
60
61 print "BUILDING: ", old_path
62
63 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 @@
1# vim: set filencoding=utf8
2"""
3Blog Post Builder
4
5@author: Mike Crute (mcrute@ag.com)
6@organization: American Greetings Interactive
7@date: June 03, 2010
8"""
9
10import os
11from functools import wraps
12from datetime import datetime
13
14# Docutils imports, crazy yo
15from docutils import nodes
16from docutils.core import Publisher, publish_string
17from docutils.transforms import Transform
18from docutils.io import NullOutput, FileInput
19from docutils.parsers.rst import Parser as RSTParser
20from docutils.writers.html4css1 import Writer as HTMLWriter
21from docutils.readers.standalone import Reader as StandaloneReader
22
23
24class BlogMetaTransform(Transform):
25 """
26 Removes metadata tags from the document tree.
27
28 This transformer removes the metadata nodes from the document tree
29 and places them in a blog_meta dictionary on the document object.
30 This happens before rendering so the meta won't show up in the output.
31 """
32
33 default_priority = 360 # Fuck if I know, same as the PEP header transform
34
35 def __init__(self, *args, **kwargs):
36 Transform.__init__(self, *args, **kwargs)
37
38 self.meta = self.document.blog_meta = {
39 'tags': [],
40 }
41
42 def apply(self):
43 docinfo = None
44
45 # One to get the docinfo and title
46 # We need a copy of the document as a list so we can modify it
47 # without messing up iteration.
48 for node in list(self.document):
49 if isinstance(node, nodes.docinfo):
50 docinfo = node
51 self.document.remove(node)
52
53 if isinstance(node, nodes.title):
54 self.meta['title'] = unicode(node[0])
55 self.document.remove(node)
56
57 # And one to process the docinfo
58 for node in docinfo:
59 if isinstance(node, nodes.author):
60 self._handle_author(node)
61
62 if isinstance(node, nodes.date):
63 self._handle_date(node)
64
65 if isinstance(node, nodes.field):
66 self._handle_field(node)
67
68 def _handle_author(self, node):
69 self.meta['author'] = Author(node[0]['name'], node[0]['refuri'])
70
71 def _handle_date(self, node):
72 raw_date = unicode(node[0])
73 self.meta['post_date'] = datetime.strptime(raw_date,
74 '%a %b %d %H:%M:%S %Y')
75
76 def _handle_field(self, node):
77 name = node[0][0]
78 value = unicode(node[1][0][0])
79
80 if name == 'Tag':
81 self.meta['tags'].append(value)
82
83
84
85class BlogPostReader(StandaloneReader):
86 """
87 Post reader for blog posts.
88
89 This exists only so that we can append our custom blog
90 transformers on to the regular ones.
91 """
92
93 def get_transforms(self):
94 return StandaloneReader.get_transforms(self) + [
95 BlogMetaTransform,
96 ]
97
98
99class Author(object):
100 """
101 Representation of the author information for a blog post.
102 """
103
104 def __init__(self, name, email):
105 self.name = name
106 self.email = email
107
108 if email.startswith('mailto:'):
109 self.email = email[len('mailto:'):]
110
111 def __str__(self):
112 return '{0} <{1}>'.format(self.name, self.email)
113
114
115class BlogPost(object):
116 """
117 Representation of a blog post.
118
119 Constructed from a docutils dom version of the blog post.
120 """
121
122 def __init__(self, title, post_date, author, tags, contents=None):
123 self.title = title
124 self.post_date = post_date
125 self.author = author
126 self.tags = tags
127 self.contents = contents
128 self._filename = None
129
130 @property
131 def filename(self):
132 return os.path.basename(self._filename)
133
134 @filename.setter
135 def filename(self, value):
136 self._filename = value
137
138 @property
139 def pretty_date(self):
140 return self.post_date.strftime("%B %d, %Y")
141
142 @classmethod
143 def from_file(cls, filename):
144 """
145 Loads a file from disk, parses it and constructs a new BlogPost.
146
147 This method reflects a bit of the insanity of docutils. Basically
148 this is just the docutils.core.publish_doctree function with some
149 modifications to use an html writer and to load a file instead of
150 a string.
151 """
152 pub = Publisher(destination_class=NullOutput,
153 source=FileInput(source_path=filename),
154 reader=BlogPostReader(), writer=HTMLWriter(),
155 parser=RSTParser())
156
157 pub.get_settings() # This is not sane.
158 pub.settings.traceback = True # Damnit
159 pub.publish()
160
161 meta = pub.document.blog_meta
162 post = cls(meta['title'], meta['post_date'], meta['author'],
163 meta['tags'], pub.writer.parts['html_body'])
164
165 post.filename = filename
166
167 return post
168
169
170def load_post_index(directory='.'):
171 """
172 Scan the current directory for rst files and build an index.
173 """
174 posts = []
175 for filename in os.listdir(directory):
176 if not filename.endswith('.rst'):
177 continue
178
179 filename = os.path.join(directory, filename)
180 posts.append(BlogPost.from_file(filename))
181
182 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 @@
1# vim: set filencoding=utf8
2"""
3Blog Builder
4
5@author: Mike Crute (mcrute@ag.com)
6@organization: American Greetings Interactive
7@date: June 04, 2010
8"""
9
10import os
11import operator
12from datetime import datetime
13from collections import defaultdict
14from site_builder import get_template, get_output_name
15from blog import load_post_index
16from feeds import Atom1Feed
17
18
19def build_feed(output_dir, post_index):
20 page_name = os.path.join(output_dir, 'feed.atom')
21 feed = Atom1Feed(post_index, "The Random Thoughts of a Programmer",
22 "http://mike.crute.org/blog/feed",
23 post_index[0].post_date,
24 "http://mike.crute.org/blog")
25
26 open(page_name, 'w').write(feed.get_feed())
27
28
29def build_tags(output_dir, post_index):
30 tag_index = defaultdict(list)
31
32 template = get_template('blog_tags.html')
33 page_name = os.path.join(output_dir, 'tags.html')
34
35 for post in post_index:
36 for tag in post.tags:
37 tag_index[tag].append(post)
38
39 tag_index = sorted(tag_index.items())
40 open(page_name, 'w').write(template.render(posts=tag_index,
41 build_date=datetime.now().strftime("%B %d, %Y")))
42
43
44def build_archive(output_dir, post_index):
45 date_index = defaultdict(list)
46
47 template = get_template('blog_archive.html')
48 page_name = os.path.join(output_dir, 'archive.html')
49
50 for post in post_index:
51 date_index[post.post_date.year].append(post)
52
53 date_index = sorted(date_index.items(), reverse=True)
54 open(page_name, 'w').write(template.render(posts=date_index,
55 build_date=datetime.now().strftime("%B %d, %Y")))
56
57
58def build_index(output_dir, post_index):
59 template = get_template('blog_index.html')
60 page_name = os.path.join(output_dir, 'index.html')
61
62 open(page_name, 'w').write(template.render(posts=post_index[:3],
63 build_date=datetime.now().strftime("%B %d, %Y")))
64
65
66def build_blog(base_dir, output_dir):
67 post_index = load_post_index(base_dir)
68 post_index.sort(key=operator.attrgetter('post_date'), reverse=True)
69
70 try:
71 os.makedirs(output_dir)
72 except OSError:
73 pass # directory already exists
74
75 for post in post_index:
76 template = get_template('blog_post.html')
77
78 out_filename = os.path.join(output_dir, post.filename)
79 out_filename = out_filename[:-len('rst')] + 'html'
80
81 print "BUILDING BLOG: ", out_filename
82
83 open(out_filename, 'w').write(template.render(post=post))
84
85 build_index(output_dir, post_index)
86 build_archive(output_dir, post_index)
87 build_feed(output_dir, post_index)
88 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 @@
1# vim: set filencoding=utf8
2"""
3Atom Feed Writer
4
5@author: Mike Crute (mcrute@ag.com)
6@organization: American Greetings Interactive
7@date: June 04, 2010
8"""
9
10#from io import StringIO
11from StringIO import StringIO
12from xml.sax.saxutils import XMLGenerator
13
14
15class SimpleXMLGenerator(XMLGenerator):
16
17 def __init__(self, encoding='utf-8'):
18 self.output = StringIO()
19 XMLGenerator.__init__(self, out=self.output, encoding=encoding)
20
21 def get_contents(self):
22 return self.output.getvalue()
23
24 def startElement(self, tag, attrs=None):
25 attrs = attrs if attrs else {}
26 return XMLGenerator.startElement(self, tag, attrs)
27
28 def addElement(self, tag, contents=None, attrs=None):
29 attrs = attrs if attrs else {}
30 self.startElement(tag, attrs)
31 if contents:
32 self.characters(contents)
33 self.endElement(tag)
34
35
36class Atom1Feed(object):
37
38 def __init__(self, posts, title, feed_url, updated, blog_url,
39 post_filter=None):
40 self.posts = posts
41 self.title = title
42 self.feed_url = feed_url
43 self.updated = updated
44 self.blog_url = blog_url
45 self.handler = SimpleXMLGenerator()
46
47 if not post_filter:
48 post_filter = lambda post: True
49
50 self.post_filter = post_filter
51
52 def _format_time(self, timeobj):
53 return timeobj.strftime("%Y-%m-%dT%H:%M:%SZ")
54
55 def get_feed(self):
56 self.handler.startDocument()
57 self.handler.startElement('feed', {
58 'xmlns': 'http://www.w3.org/2005/Atom' })
59
60 self.add_root_elements()
61
62 for post in self.posts:
63 if not self.post_filter(post):
64 continue
65
66 self.add_post(post)
67
68 self.handler.endElement('feed')
69
70 return self.handler.get_contents()
71
72 def add_root_elements(self):
73 self.handler.addElement('title', self.title)
74 self.handler.addElement('updated', self._format_time(self.updated))
75 self.handler.addElement('id', self.feed_url)
76 self.handler.addElement('link', attrs={
77 'rel': 'alternate',
78 'type': 'text/html',
79 'href': self.blog_url })
80 self.handler.addElement('link', attrs={
81 'rel': 'self',
82 'type': 'application/atom+xml',
83 'href': self.feed_url })
84
85 def add_post(self, post):
86 handler = self.handler
87
88 handler.startElement('entry')
89
90 handler.startElement('author')
91 handler.addElement('name', post.author.name)
92 handler.addElement('email', post.author.email)
93 handler.endElement('author')
94
95 post_href = '{0}/{1}'.format(self.blog_url, post.filename)
96
97 handler.addElement('title', post.title)
98 handler.addElement('link', attrs={
99 'rel': 'alternate',
100 'type': 'text/html',
101 'href': post_href })
102 handler.addElement('id', post_href)
103 handler.addElement('updated', self._format_time(post.post_date))
104 handler.addElement('published', self._format_time(post.post_date))
105
106 for tag in post.tags:
107 handler.addElement('category', attrs={ 'term': tag })
108
109 handler.addElement('content', post.contents, attrs={ 'type': 'html' })
110
111 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 @@
1def build():
2 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 @@
1# vim: set filencoding=utf8
2"""
3Robots.txt Generator
4
5@author: Mike Crute (mcrute@ag.com)
6@organization: American Greetings Interactive
7@date: June 05, 2010
8"""
9
10"""
11Sitemap: http://mike.crute.org/sitemap.xml
12
13User-agent: *
14Disallow: /blog/wp-admin/
15"""
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 @@
1# vim: set filencoding=utf8
2"""
3Sitemap Generator
4
5@author: Mike Crute (mcrute@ag.com)
6@organization: American Greetings Interactive
7@date: June 05, 2010
8"""
9
10"""
11<?xml version="1.0" encoding="UTF-8"?>
12<?xml-stylesheet type="text/xsl" href="/blog/wp-content/plugins/google-sitemap-generator/sitemap.xsl"?>
13<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
14 <url>
15 <loc>http://mike.crute.org/blog/</loc>
16 <lastmod>2009-09-09T13:28:46+00:00</lastmod>
17 <changefreq>daily</changefreq>
18 <priority>1.0</priority>
19 </url>
20</urlset>
21"""
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 @@
1{% extends "page.html" %}
2
3{% set is_blog = True %}
4
5{% block contents %}
6{% for year, post_set in posts %}
7<h2>{{ year }}</h2>
8<ul>
9 {% for post in post_set %}
10 <li><a href="{{ post.filename|replace('.rst', '.html') }}">{{ post.title }}</a></li>
11 {% endfor %}
12</ul>
13{% endfor %}
14{% 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 @@
1{% extends "page.html" %}
2
3{% set is_blog = True %}
4
5{% block contents %}
6{% for post in posts %}
7<h1 class="title">{{ post.title }}</h1>
8<p><span class="label">Posted:</span> {{ post.pretty_date }}</span></p>
9{{ post.contents }}
10{% if not loop.last %}
11<hr />
12{% endif %}
13{% endfor %}
14{% endblock %}
15
16{% block update_date %}
17 <p class="updated"><span class="label">Page last updated:</span> {{ posts[0].pretty_date }}</p>
18{% 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 @@
1{% extends "page.html" %}
2
3{% set is_blog = True %}
4
5{% block contents %}
6<h1 class="title">{{ post.title }}</h1>
7{{ post.contents }}
8
9{# Maybe enable comments... some day...
10<hr />
11<h2>Comments</h2>
12<hr />
13<div id="disqus_thread"></div>
14<script type="text/javascript">
15(function() {
16var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
17dsq.src = 'http://randomthoughtsofaprogrammer.disqus.com/embed.js';
18(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
19})();
20</script>
21#}
22{% endblock %}
23
24{% block update_date %}
25 <p class="updated"><span class="label">Page last updated:</span> {{ post.pretty_date }}</p>
26{% 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 @@
1{% extends "page.html" %}
2
3{% set is_blog = True %}
4
5{% block contents %}
6<h2>All Tags</h2>
7<p>
8{%- for tag, post_set in posts %}
9<a href="#{{ tag }}">{{ tag }}</a>
10{% endfor %}
11</p>
12
13
14{% for tag, post_set in posts %}
15<h2 id="{{ tag }}">{{ tag }}</h2>
16<ul>
17 {% for post in post_set %}
18 <li><a href="{{ post.filename|replace('.rst', '.html') }}">{{ post.title }}</a></li>
19 {% endfor %}
20</ul>
21{% endfor %}
22{% 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 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
3<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
4<head>
5<meta http-equiv="content-type" content="text/html;charset=utf-8" />
6<meta name="google-site-verification" content="Ycqn8jLbKJrWCDs97nD20cjYEhFf875j1teoofSd6Sc" />
7<meta name="generator" content="Ventriloquy 0.1" />
8<meta name="author" content="Michael Crute" />
9<title>The Random Thoughts of a Programmer</title>
10<style type="text/css">@import url('/resources/site-min.css');</style>
11</head>
12<body>
13
14<div id="header">
15 <h1 class="headline"><a href="/">mike<span>.</span>crute<span>.org</span></a></h1>
16</div>
17
18<div id="contents">
19
20{% block contents %}
21{{ contents }}
22{% endblock %}
23
24</div>
25
26<div id="footer">
27{% if is_blog %}
28<div id="footer-blog">
29 <ul class="links">
30 <li><span class="label">Blog Links:</span></li>
31 <li><a href="archive">Archive</a></li>
32 <li><a href="tags">Tags</a></li>
33 <li><a href="feed">Atom Feed</a></li>
34 </ul>
35</div>
36{% endif %}
37
38<div id="footer-page">
39 <ul class="links">
40 <li><span class="label">Site Links:</span></li>
41 <li><a href="/">Home</a></li>
42 <li><a href="/blog">Blog</a></li>
43 <li><a href="/personal_blog">Personal Blog</a></li>
44 <li><a href="/code">Code</a></li>
45 <li><a href="http://twitter.com/mcrute">Twitter</a></li>
46 </ul>
47
48 {% block update_date %}
49 <p class="updated"><span class="label">Page last updated:</span> {{ build_date }}</p>
50 {% endblock %}
51 <p class="copyright">Copyright &copy; 2010 Michael E. Crute &lt;<a href="mailto:mcrute@gmail.com">mcrute@gmail.com</a>&gt;. Contact me for other uses.</p>
52 <p class="poweredby">This site powered by <a href="http://www.vim.org">VIM</a>, <a href="http://docutils.sourceforge.net">ReStructuredText</a> and <a href="http://python.org">Python</a>.</p>
53</div>
54</div>
55
56</body>
57</html>