diff options
Diffstat (limited to 'site_builder/blog.py')
-rw-r--r-- | site_builder/blog.py | 182 |
1 files changed, 182 insertions, 0 deletions
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 | """ | ||
3 | Blog Post Builder | ||
4 | |||
5 | @author: Mike Crute (mcrute@ag.com) | ||
6 | @organization: American Greetings Interactive | ||
7 | @date: June 03, 2010 | ||
8 | """ | ||
9 | |||
10 | import os | ||
11 | from functools import wraps | ||
12 | from datetime import datetime | ||
13 | |||
14 | # Docutils imports, crazy yo | ||
15 | from docutils import nodes | ||
16 | from docutils.core import Publisher, publish_string | ||
17 | from docutils.transforms import Transform | ||
18 | from docutils.io import NullOutput, FileInput | ||
19 | from docutils.parsers.rst import Parser as RSTParser | ||
20 | from docutils.writers.html4css1 import Writer as HTMLWriter | ||
21 | from docutils.readers.standalone import Reader as StandaloneReader | ||
22 | |||
23 | |||
24 | class 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 | |||
85 | class 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 | |||
99 | class 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 | |||
115 | class 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 | |||
170 | def 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 | ||