|
1 #!/usr/bin/env python |
|
2 |
|
3 # Author: David Goodger |
|
4 # Contact: goodger@users.sourceforge.net |
|
5 # Revision: $Revision: 1.1 $ |
|
6 # Date: $Date: 2009/02/05 23:03:30 $ |
|
7 # Copyright: This module has been placed in the public domain. |
|
8 |
|
9 # Addapted by William Caban to look for .rst by default instead of .txt |
|
10 # added docutils availability |
|
11 |
|
12 """ |
|
13 Generates .html from all the .rst files in a directory. |
|
14 |
|
15 Ordinary .rst files are understood to be standalone reStructuredText. |
|
16 Files named ``pep-*.rst`` are interpreted as reStructuredText PEPs. |
|
17 """ |
|
18 # Once PySource is here, build .html from .py as well. |
|
19 |
|
20 __docformat__ = 'reStructuredText' |
|
21 |
|
22 |
|
23 try: |
|
24 import locale |
|
25 locale.setlocale(locale.LC_ALL, '') |
|
26 except: |
|
27 pass |
|
28 |
|
29 import sys |
|
30 import os |
|
31 import os.path |
|
32 import copy |
|
33 try: |
|
34 import docutils |
|
35 from docutils import ApplicationError |
|
36 from docutils import core, frontend |
|
37 from docutils.parsers import rst |
|
38 from docutils.readers import standalone, pep |
|
39 from docutils.writers import html4css1, pep_html |
|
40 except: |
|
41 print "################################################################" |
|
42 print "# You need 'docutils' installed to execute this program. #" |
|
43 print "# 'docutils' is available from http://docutils.sourceforge.net #" |
|
44 print "################################################################" |
|
45 sys.exit(1) |
|
46 |
|
47 |
|
48 usage = '%prog [options] [<directory> ...]' |
|
49 description = ('Generates .html from all the reStructuredText .rst files ' |
|
50 '(including PEPs) in each <directory> ' |
|
51 '(default is the current directory).') |
|
52 |
|
53 |
|
54 class SettingsSpec(docutils.SettingsSpec): |
|
55 |
|
56 """ |
|
57 Runtime settings & command-line options for the front end. |
|
58 """ |
|
59 |
|
60 # Can't be included in OptionParser below because we don't want to |
|
61 # override the base class. |
|
62 settings_spec = ( |
|
63 'Build-HTML Options', |
|
64 None, |
|
65 (('Recursively scan subdirectories for files to process. This is ' |
|
66 'the default.', |
|
67 ['--recurse'], |
|
68 {'action': 'store_true', 'default': 1, |
|
69 'validator': frontend.validate_boolean}), |
|
70 ('Generate output files in the specified directory. The default is ' |
|
71 'to put them in the same directory of the input files.', |
|
72 ['--outpath'], {'metavar': '<directory>', 'type': 'string'}), |
|
73 ('Do not scan subdirectories for files to process.', |
|
74 ['--local'], {'dest': 'recurse', 'action': 'store_false'}), |
|
75 ('Do not process files in <directory>. This option may be used ' |
|
76 'more than once to specify multiple directories.', |
|
77 ['--prune'], |
|
78 {'metavar': '<directory>', 'action': 'append', |
|
79 'validator': frontend.validate_colon_separated_string_list}), |
|
80 ('Work silently (no progress messages). Independent of "--quiet".', |
|
81 ['--silent'], |
|
82 {'action': 'store_true', 'validator': frontend.validate_boolean}),)) |
|
83 |
|
84 relative_path_settings = ('prune',) |
|
85 config_section = 'buildhtml application' |
|
86 config_section_dependencies = ('applications',) |
|
87 |
|
88 |
|
89 class OptionParser(frontend.OptionParser): |
|
90 |
|
91 """ |
|
92 Command-line option processing for the ``buildhtml.py`` front end. |
|
93 """ |
|
94 |
|
95 def check_values(self, values, args): |
|
96 frontend.OptionParser.check_values(self, values, args) |
|
97 values._source = None |
|
98 return values |
|
99 |
|
100 def check_args(self, args): |
|
101 source = destination = None |
|
102 if args: |
|
103 self.values._directories = args |
|
104 else: |
|
105 self.values._directories = [os.getcwd()] |
|
106 return source, destination |
|
107 |
|
108 |
|
109 class Struct: |
|
110 |
|
111 """Stores data attributes for dotted-attribute access.""" |
|
112 |
|
113 def __init__(self, **keywordargs): |
|
114 self.__dict__.update(keywordargs) |
|
115 |
|
116 |
|
117 class Builder: |
|
118 |
|
119 def __init__(self): |
|
120 self.publishers = { |
|
121 '': Struct(components=(pep.Reader, rst.Parser, pep_html.Writer, |
|
122 SettingsSpec)), |
|
123 '.rst': Struct(components=(rst.Parser, standalone.Reader, |
|
124 html4css1.Writer, SettingsSpec), |
|
125 reader_name='standalone', |
|
126 writer_name='html'), |
|
127 'PEPs': Struct(components=(rst.Parser, pep.Reader, |
|
128 pep_html.Writer, SettingsSpec), |
|
129 reader_name='pep', |
|
130 writer_name='pep_html')} |
|
131 """Publisher-specific settings. Key '' is for the front-end script |
|
132 itself. ``self.publishers[''].components`` must contain a superset of |
|
133 all components used by individual publishers.""" |
|
134 |
|
135 self.setup_publishers() |
|
136 |
|
137 def setup_publishers(self): |
|
138 """ |
|
139 Manage configurations for individual publishers. |
|
140 |
|
141 Each publisher (combination of parser, reader, and writer) may have |
|
142 its own configuration defaults, which must be kept separate from those |
|
143 of the other publishers. Setting defaults are combined with the |
|
144 config file settings and command-line options by |
|
145 `self.get_settings()`. |
|
146 """ |
|
147 for name, publisher in self.publishers.items(): |
|
148 option_parser = OptionParser( |
|
149 components=publisher.components, read_config_files=1, |
|
150 usage=usage, description=description) |
|
151 publisher.option_parser = option_parser |
|
152 publisher.setting_defaults = option_parser.get_default_values() |
|
153 frontend.make_paths_absolute(publisher.setting_defaults.__dict__, |
|
154 option_parser.relative_path_settings) |
|
155 publisher.config_settings = ( |
|
156 option_parser.get_standard_config_settings()) |
|
157 self.settings_spec = self.publishers[''].option_parser.parse_args( |
|
158 values=frontend.Values()) # no defaults; just the cmdline opts |
|
159 self.initial_settings = self.get_settings('') |
|
160 |
|
161 def get_settings(self, publisher_name, directory=None): |
|
162 """ |
|
163 Return a settings object, from multiple sources. |
|
164 |
|
165 Copy the setting defaults, overlay the startup config file settings, |
|
166 then the local config file settings, then the command-line options. |
|
167 Assumes the current directory has been set. |
|
168 """ |
|
169 publisher = self.publishers[publisher_name] |
|
170 settings = frontend.Values(publisher.setting_defaults.__dict__) |
|
171 settings.update(publisher.config_settings, publisher.option_parser) |
|
172 if directory: |
|
173 local_config = publisher.option_parser.get_config_file_settings( |
|
174 os.path.join(directory, 'docutils.conf')) |
|
175 frontend.make_paths_absolute( |
|
176 local_config, publisher.option_parser.relative_path_settings, |
|
177 directory) |
|
178 settings.update(local_config, publisher.option_parser) |
|
179 settings.update(self.settings_spec.__dict__, publisher.option_parser) |
|
180 return settings |
|
181 |
|
182 def run(self, directory=None, recurse=1): |
|
183 recurse = recurse and self.initial_settings.recurse |
|
184 if directory: |
|
185 self.directories = [directory] |
|
186 elif self.settings_spec._directories: |
|
187 self.directories = self.settings_spec._directories |
|
188 else: |
|
189 self.directories = [os.getcwd()] |
|
190 for directory in self.directories: |
|
191 os.path.walk(directory, self.visit, recurse) |
|
192 |
|
193 def visit(self, recurse, directory, names): |
|
194 settings = self.get_settings('', directory) |
|
195 if settings.prune and (os.path.abspath(directory) in settings.prune): |
|
196 print >>sys.stderr, '/// ...Skipping directory (pruned):', directory |
|
197 sys.stderr.flush() |
|
198 names[:] = [] |
|
199 return |
|
200 if not self.initial_settings.silent: |
|
201 print >>sys.stderr, '/// Processing directory:', directory |
|
202 sys.stderr.flush() |
|
203 prune = 0 |
|
204 for name in names: |
|
205 if name.endswith('.rst'): |
|
206 prune = self.process_rst(directory, name) |
|
207 if prune: |
|
208 break |
|
209 if not recurse: |
|
210 del names[:] |
|
211 |
|
212 def process_rst(self, directory, name): |
|
213 if name.startswith('pep-'): |
|
214 publisher = 'PEPs' |
|
215 else: |
|
216 publisher = '.rst' |
|
217 settings = self.get_settings(publisher, directory) |
|
218 pub_struct = self.publishers[publisher] |
|
219 if settings.prune and (directory in settings.prune): |
|
220 return 1 |
|
221 settings._source = os.path.normpath(os.path.join(directory, name)) |
|
222 settings._destination = settings._source[:-4]+'.html' |
|
223 if settings.outpath: |
|
224 # FIXME: we should probably try and recreate the exising structure here, |
|
225 # but that's more work than we need right now. |
|
226 settings._destination = os.path.join(settings.outpath, os.path.basename(settings._destination)) |
|
227 if not self.initial_settings.silent: |
|
228 print >>sys.stderr, ' ::: Processing:', name |
|
229 sys.stderr.flush() |
|
230 try: |
|
231 core.publish_file(source_path=settings._source, |
|
232 destination_path=settings._destination, |
|
233 reader_name=pub_struct.reader_name, |
|
234 parser_name='restructuredtext', |
|
235 writer_name=pub_struct.writer_name, |
|
236 settings=settings) |
|
237 except ApplicationError, error: |
|
238 print >>sys.stderr, (' Error (%s): %s' |
|
239 % (error.__class__.__name__, error)) |
|
240 |
|
241 |
|
242 if __name__ == "__main__": |
|
243 Builder().run() |