Release 0.5: added empty "Changes Since 0.5" section
[docutils.git] / tools / buildhtml.py
blobe5fda8293b30ea94d9541fdc62c419f32dd8e637
1 #!/usr/bin/env python
3 # $Id$
4 # Author: David Goodger <goodger@python.org>
5 # Copyright: This module has been placed in the public domain.
7 """
8 Generates .html from all the .txt files in a directory.
10 Ordinary .txt files are understood to be standalone reStructuredText.
11 Files named ``pep-*.txt`` are interpreted as reStructuredText PEPs.
12 """
13 # Once PySource is here, build .html from .py as well.
15 __docformat__ = 'reStructuredText'
18 try:
19 import locale
20 locale.setlocale(locale.LC_ALL, '')
21 except:
22 pass
24 import sys
25 import os
26 import os.path
27 import copy
28 from fnmatch import fnmatch
29 import docutils
30 from docutils import ApplicationError
31 from docutils import core, frontend, utils
32 from docutils.parsers import rst
33 from docutils.readers import standalone, pep
34 from docutils.writers import html4css1, pep_html
37 usage = '%prog [options] [<directory> ...]'
38 description = ('Generates .html from all the reStructuredText .txt files '
39 '(including PEPs) in each <directory> '
40 '(default is the current directory).')
43 class SettingsSpec(docutils.SettingsSpec):
45 """
46 Runtime settings & command-line options for the front end.
47 """
49 # Can't be included in OptionParser below because we don't want to
50 # override the base class.
51 settings_spec = (
52 'Build-HTML Options',
53 None,
54 (('Recursively scan subdirectories for files to process. This is '
55 'the default.',
56 ['--recurse'],
57 {'action': 'store_true', 'default': 1,
58 'validator': frontend.validate_boolean}),
59 ('Do not scan subdirectories for files to process.',
60 ['--local'], {'dest': 'recurse', 'action': 'store_false'}),
61 ('Do not process files in <directory>. This option may be used '
62 'more than once to specify multiple directories.',
63 ['--prune'],
64 {'metavar': '<directory>', 'action': 'append',
65 'validator': frontend.validate_colon_separated_string_list}),
66 ('Recursively ignore files or directories matching any of the given '
67 'wildcard (shell globbing) patterns (separated by colons). '
68 'Default: ".svn:CVS"',
69 ['--ignore'],
70 {'metavar': '<patterns>', 'action': 'append',
71 'default': ['.svn', 'CVS'],
72 'validator': frontend.validate_colon_separated_string_list}),
73 ('Work silently (no progress messages). Independent of "--quiet".',
74 ['--silent'],
75 {'action': 'store_true', 'validator': frontend.validate_boolean}),))
77 relative_path_settings = ('prune',)
78 config_section = 'buildhtml application'
79 config_section_dependencies = ('applications',)
82 class OptionParser(frontend.OptionParser):
84 """
85 Command-line option processing for the ``buildhtml.py`` front end.
86 """
88 def check_values(self, values, args):
89 frontend.OptionParser.check_values(self, values, args)
90 values._source = None
91 return values
93 def check_args(self, args):
94 source = destination = None
95 if args:
96 self.values._directories = args
97 else:
98 self.values._directories = [os.getcwd()]
99 return source, destination
102 class Struct:
104 """Stores data attributes for dotted-attribute access."""
106 def __init__(self, **keywordargs):
107 self.__dict__.update(keywordargs)
110 class Builder:
112 def __init__(self):
113 self.publishers = {
114 '': Struct(components=(pep.Reader, rst.Parser, pep_html.Writer,
115 SettingsSpec)),
116 '.txt': Struct(components=(rst.Parser, standalone.Reader,
117 html4css1.Writer, SettingsSpec),
118 reader_name='standalone',
119 writer_name='html'),
120 'PEPs': Struct(components=(rst.Parser, pep.Reader,
121 pep_html.Writer, SettingsSpec),
122 reader_name='pep',
123 writer_name='pep_html')}
124 """Publisher-specific settings. Key '' is for the front-end script
125 itself. ``self.publishers[''].components`` must contain a superset of
126 all components used by individual publishers."""
128 self.setup_publishers()
130 def setup_publishers(self):
132 Manage configurations for individual publishers.
134 Each publisher (combination of parser, reader, and writer) may have
135 its own configuration defaults, which must be kept separate from those
136 of the other publishers. Setting defaults are combined with the
137 config file settings and command-line options by
138 `self.get_settings()`.
140 for name, publisher in self.publishers.items():
141 option_parser = OptionParser(
142 components=publisher.components, read_config_files=1,
143 usage=usage, description=description)
144 publisher.option_parser = option_parser
145 publisher.setting_defaults = option_parser.get_default_values()
146 frontend.make_paths_absolute(publisher.setting_defaults.__dict__,
147 option_parser.relative_path_settings)
148 publisher.config_settings = (
149 option_parser.get_standard_config_settings())
150 self.settings_spec = self.publishers[''].option_parser.parse_args(
151 values=frontend.Values()) # no defaults; just the cmdline opts
152 self.initial_settings = self.get_settings('')
154 def get_settings(self, publisher_name, directory=None):
156 Return a settings object, from multiple sources.
158 Copy the setting defaults, overlay the startup config file settings,
159 then the local config file settings, then the command-line options.
160 Assumes the current directory has been set.
162 publisher = self.publishers[publisher_name]
163 settings = frontend.Values(publisher.setting_defaults.__dict__)
164 settings.update(publisher.config_settings, publisher.option_parser)
165 if directory:
166 local_config = publisher.option_parser.get_config_file_settings(
167 os.path.join(directory, 'docutils.conf'))
168 frontend.make_paths_absolute(
169 local_config, publisher.option_parser.relative_path_settings,
170 directory)
171 settings.update(local_config, publisher.option_parser)
172 settings.update(self.settings_spec.__dict__, publisher.option_parser)
173 return settings
175 def run(self, directory=None, recurse=1):
176 recurse = recurse and self.initial_settings.recurse
177 if directory:
178 self.directories = [directory]
179 elif self.settings_spec._directories:
180 self.directories = self.settings_spec._directories
181 else:
182 self.directories = [os.getcwd()]
183 for directory in self.directories:
184 os.path.walk(directory, self.visit, recurse)
186 def visit(self, recurse, directory, names):
187 settings = self.get_settings('', directory)
188 if settings.prune and (os.path.abspath(directory) in settings.prune):
189 print >>sys.stderr, '/// ...Skipping directory (pruned):', directory
190 sys.stderr.flush()
191 names[:] = []
192 return
193 if not self.initial_settings.silent:
194 print >>sys.stderr, '/// Processing directory:', directory
195 sys.stderr.flush()
196 # settings.ignore grows many duplicate entries as we recurse
197 # if we add patterns in config files or on the command line.
198 for pattern in utils.uniq(settings.ignore):
199 for i in range(len(names) - 1, -1, -1):
200 if fnmatch(names[i], pattern):
201 # Modify in place!
202 del names[i]
203 prune = 0
204 for name in names:
205 if name.endswith('.txt'):
206 prune = self.process_txt(directory, name)
207 if prune:
208 break
209 if not recurse:
210 del names[:]
212 def process_txt(self, directory, name):
213 if name.startswith('pep-'):
214 publisher = 'PEPs'
215 else:
216 publisher = '.txt'
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 not self.initial_settings.silent:
224 print >>sys.stderr, ' ::: Processing:', name
225 sys.stderr.flush()
226 try:
227 core.publish_file(source_path=settings._source,
228 destination_path=settings._destination,
229 reader_name=pub_struct.reader_name,
230 parser_name='restructuredtext',
231 writer_name=pub_struct.writer_name,
232 settings=settings)
233 except ApplicationError, error:
234 print >>sys.stderr, (' Error (%s): %s'
235 % (error.__class__.__name__, error))
238 if __name__ == "__main__":
239 Builder().run()