4 import os
, time
, datetime
5 from os
.path
import join
, abspath
6 from optparse
import OptionParser
8 from docutils
import core
, nodes
9 from docutils
.parsers
import rst
10 from docutils
.writers
.html4css1
import Writer
, HTMLTranslator
12 from mako
.template
import Template
13 from mako
.lookup
import TemplateLookup
15 from pygments
import highlight
16 from pygments
.lexers
import get_lexer_by_name
, TextLexer
17 from pygments
.formatters
import HtmlFormatter
21 # Start with --help to get help
23 oparser
= OptionParser()
24 oparser
.set_defaults(**dict(
25 rstdir
= './example/',
26 templatedir
= './example/',
27 baseurl
= 'http://localhost:8080',
28 title
= 'example.com',
31 disqus_forum
= 'tst' # DEBUG
33 oparser
.add_option('-r', '--rstdir', dest
='rstdir',
34 help='Directory where source .rst files are found')
35 oparser
.add_option('-t', '--templatedir', dest
='templatedir',
36 help='Directory where mako templates can be found')
37 oparser
.add_option('-b', '--baseurl', dest
='baseurl',
38 help='The base URL of the site')
39 oparser
.add_option('-T', '--title', dest
='title',
40 help='Site title that goes into <title>')
41 oparser
.add_option('', '--regen', dest
='regen', action
="store_true",
42 help='Regenerate the whole site (all files)')
43 oparser
.add_option('-d', '--disqus-forum', dest
='disqus_forum',
44 help='The name of the disqus forum. For example, if http://foo.disqus.com is the forum, then do --disqus-forum=foo')
46 (options
, args
) = oparser
.parse_args()
47 options
.rstdir
= abspath(options
.rstdir
)
48 options
.templatedir
= abspath(options
.templatedir
)
49 print 'using options: %s' % options
52 # Pygments syntax-highlighting for ReST
54 # http://dev.pocoo.org/projects/pygments/browser/external/rst-directive.py
57 DEFAULT
= HtmlFormatter(noclasses
=INLINESTYLES
)
58 # Add name -> formatter pairs for every variant you want to use
60 # 'linenos': HtmlFormatter(noclasses=INLINESTYLES, linenos=True),
63 def pygments_directive(name
, arguments
, options
, content
, lineno
,
64 content_offset
, block_text
, state
, state_machine
):
66 lexer
= get_lexer_by_name(arguments
[0])
68 # no lexer found - use the text one instead of an exception
70 # take an arbitrary option if more than one is given
71 formatter
= options
and VARIANTS
[options
.keys()[0]] or DEFAULT
72 parsed
= highlight(u
'\n'.join(content
), lexer
, formatter
)
73 return [nodes
.raw('', parsed
, format
='html')]
75 pygments_directive
.arguments
= (1, 0, 1)
76 pygments_directive
.content
= 1
77 pygments_directive
.options
= dict([(key
, directives
.flag
) for key
in VARIANTS
])
79 rst
.directives
.register_directive('sourcecode', pygments_directive
)
82 def htmlpath(rstfile
):
83 """Return the .html path for the given .rst path"""
84 assert rstfile
.endswith('.rst'), '<%s> is not a rst path' % rstfile
85 return rstfile
[:-4] + '.html'
90 def __init__(self
, rstfile
):
91 self
.rstfile
= rstfile
93 writer
.translator_class
= DocInfoProxy
# see comment for `DocInfoProxy`
95 parts
= core
.publish_parts(
96 source
=open(rstfile
).read(),
97 source_path
=rstfile
, writer
=writer
)
98 self
.title
, self
.fragment
= parts
['title'], parts
['fragment']
99 self
.meta
= DocInfoProxy
.ITEMS
.copy()
102 if '|' in self
.meta
['date']:
103 format
= '%Y-%m-%d|%H:%M'
107 return datetime
.datetime(
108 *time
.strptime(self
.meta
['date'],
112 tags
= self
.meta
.get('tags', '').split()
117 path_info
= htmlpath(self
.rstfile
[len(options
.rstdir
):])
118 return options
.baseurl
+ path_info
120 def commentable(self
):
123 class BlogPost(Page
):
125 def commentable(self
):
128 def __cmp__(self
, other
):
129 return cmp(self
.date(), other
.date())
132 return '<BlogPost "%s" on %s>' % (self
.title
, self
.date())
135 # This is a temporary hack - to extract the 'custom fields' from
136 # docutil's `docinfo` node.
138 # `HTMLTranslator` happens to process the docinfo tree and so we can steal
139 # the key,value pairs of custom fields from the `visit_field_name` function.
140 class DocInfoProxy(HTMLTranslator
):
144 def __init__(self
, *args
, **kwargs
):
145 HTMLTranslator
.__init
__(self
, *args
, **kwargs
)
146 DocInfoProxy
.ITEMS
= {}
149 # Custom fields are processed here (Tags and so on)
150 def visit_field_name(self
, node
):
151 key
= node
.astext().lower()
152 value
= node
.parent
.children
[1].astext() # from sibling
153 # print 'meta[%s] = {%s}' % (key, value)
154 DocInfoProxy
.ITEMS
[key
] = value
155 return HTMLTranslator
.visit_field_name(self
, node
)
157 # Standard fields are processed here (Date, Author and so on)
158 def visit_docinfo_item(self
, node
, name
, meta
=1):
160 value
= node
.parent
.children
[0].astext() # from sibling
161 # print '*meta[%s] = {%s}' % (key, value)
162 DocInfoProxy
.ITEMS
[key
] = value
163 return HTMLTranslator
.visit_docinfo_item(self
, node
, name
, meta
)
166 # gnu make like functinality
167 def last_modified(filename
):
168 return os
.stat(filename
).st_mtime
170 def make(target
, deps
, content_generator
, *args
, **kwargs
):
171 if options
.regen
or \
172 True in [last_modified(target
) < last_modified(d
) for d
in deps
]:
175 content
= content_generator(*args
, **kwargs
)
176 open(target
, 'w').write(content
)
179 def rstfiles(directory
):
180 for dir, subdirs
, files
in os
.walk(directory
):
182 if file.endswith('.rst'):
183 yield abspath(join(dir, file))
186 templates
= TemplateLookup(directories
=[options
.templatedir
])
187 def render(name
, **kwargs
):
188 return templates
.get_template(name
).render(**kwargs
)
193 if __name__
== '__main__':
195 blog_path
= abspath(join(options
.rstdir
, 'blog'))
196 for f
in rstfiles(options
.rstdir
):
199 blog_posts
.append(page
)
203 make(htmlpath(f
), [f
],
204 mako
, 'page.mako', **locals())
206 # generate blog index
209 make(join(options
.rstdir
, 'blog', 'index.html'),
210 [p
.rstfile
for p
in blog_posts
],
211 mako
, 'blog/index.mako', **locals())