4 # Author: David Goodger <goodger@python.org>
5 # Copyright: This module has been placed in the public domain.
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.
13 # Once PySource is here, build .html from .py as well.
15 __docformat__
= 'reStructuredText'
20 locale
.setlocale(locale
.LC_ALL
, '')
28 from fnmatch
import fnmatch
30 from docutils
import ApplicationError
31 from docutils
import core
, frontend
, utils
32 from docutils
.utils
.error_reporting
import ErrorOutput
, ErrorString
33 from docutils
.parsers
import rst
34 from docutils
.readers
import standalone
, pep
35 from docutils
.writers
import html4css1
, html5_polyglot
, pep_html
38 usage
= '%prog [options] [<directory> ...]'
39 description
= ('Generates .html from all the reStructuredText .txt files '
40 '(including PEPs) in each <directory> '
41 '(default is the current directory).')
44 class SettingsSpec(docutils
.SettingsSpec
):
47 Runtime settings & command-line options for the front end.
50 prune_default
= ['.hg', '.bzr', '.git', '.svn', 'CVS']
52 # Can't be included in OptionParser below because we don't want to
53 # override the base class.
57 (('Recursively scan subdirectories for files to process. This is '
60 {'action': 'store_true', 'default': 1,
61 'validator': frontend
.validate_boolean
}),
62 ('Do not scan subdirectories for files to process.',
63 ['--local'], {'dest': 'recurse', 'action': 'store_false'}),
64 ('Do not process files in <directory> (shell globbing patterns, '
65 'separated by colons). This option may be used '
66 'more than once to specify multiple directories. Default: "%s".'
67 % ':'.join(prune_default
),
69 {'metavar': '<directory>', 'action': 'append',
70 'validator': frontend
.validate_colon_separated_string_list
,
71 'default': prune_default
,}),
72 ('Recursively ignore files matching any of the given '
73 'wildcard (shell globbing) patterns (separated by colons).',
75 {'metavar': '<patterns>', 'action': 'append',
77 'validator': frontend
.validate_colon_separated_string_list
}),
78 ('Docutils writer, one of "html", "html4", "html5". '
79 'Default: "html" (use Docutils\' default HTML writer).',
81 {'metavar': '<writer>',
82 'choices': ['html', 'html4', 'html5'],
84 ('Obsoleted by "--writer".',
87 'metavar': '<writer>',
88 'choices': ['html', 'html4', 'html5'],}),
89 ('Work silently (no progress messages). Independent of "--quiet".',
91 {'action': 'store_true', 'validator': frontend
.validate_boolean
}),
92 ('Do not process files, show files that would be processed.',
94 {'action': 'store_true', 'validator': frontend
.validate_boolean
}),))
96 relative_path_settings
= ('prune',)
97 config_section
= 'buildhtml application'
98 config_section_dependencies
= ('applications',)
101 class OptionParser(frontend
.OptionParser
):
104 Command-line option processing for the ``buildhtml.py`` front end.
107 def check_values(self
, values
, args
):
108 frontend
.OptionParser
.check_values(self
, values
, args
)
109 values
._source
= None
112 def check_args(self
, args
):
113 source
= destination
= None
115 self
.values
._directories
= args
117 self
.values
._directories
= [os
.getcwd()]
118 return source
, destination
121 class Struct(object):
123 """Stores data attributes for dotted-attribute access."""
125 def __init__(self
, **keywordargs
):
126 self
.__dict
__.update(keywordargs
)
129 class Builder(object):
133 '': Struct(components
=(pep
.Reader
, rst
.Parser
, pep_html
.Writer
,
135 'html4': Struct(components
=(rst
.Parser
, standalone
.Reader
,
136 html4css1
.Writer
, SettingsSpec
),
137 reader_name
='standalone',
138 writer_name
='html4'),
139 'html5': Struct(components
=(rst
.Parser
, standalone
.Reader
,
140 html5_polyglot
.Writer
, SettingsSpec
),
141 reader_name
='standalone',
142 writer_name
='html5'),
143 'PEPs': Struct(components
=(rst
.Parser
, pep
.Reader
,
144 pep_html
.Writer
, SettingsSpec
),
146 writer_name
='pep_html')}
147 """Publisher-specific settings. Key '' is for the front-end script
148 itself. ``self.publishers[''].components`` must contain a superset of
149 all components used by individual publishers."""
151 self
.setup_publishers()
152 # default html writer (may change to html5 some time):
153 self
.publishers
['html'] = self
.publishers
['html4']
155 def setup_publishers(self
):
157 Manage configurations for individual publishers.
159 Each publisher (combination of parser, reader, and writer) may have
160 its own configuration defaults, which must be kept separate from those
161 of the other publishers. Setting defaults are combined with the
162 config file settings and command-line options by
163 `self.get_settings()`.
165 for name
, publisher
in self
.publishers
.items():
166 option_parser
= OptionParser(
167 components
=publisher
.components
, read_config_files
=1,
168 usage
=usage
, description
=description
)
169 publisher
.option_parser
= option_parser
170 publisher
.setting_defaults
= option_parser
.get_default_values()
171 frontend
.make_paths_absolute(publisher
.setting_defaults
.__dict
__,
172 option_parser
.relative_path_settings
)
173 publisher
.config_settings
= (
174 option_parser
.get_standard_config_settings())
175 self
.settings_spec
= self
.publishers
[''].option_parser
.parse_args(
176 values
=frontend
.Values()) # no defaults; just the cmdline opts
177 self
.initial_settings
= self
.get_settings('')
179 def get_settings(self
, publisher_name
, directory
=None):
181 Return a settings object, from multiple sources.
183 Copy the setting defaults, overlay the startup config file settings,
184 then the local config file settings, then the command-line options.
185 Assumes the current directory has been set.
187 publisher
= self
.publishers
[publisher_name
]
188 settings
= frontend
.Values(publisher
.setting_defaults
.__dict
__)
189 settings
.update(publisher
.config_settings
, publisher
.option_parser
)
191 local_config
= publisher
.option_parser
.get_config_file_settings(
192 os
.path
.join(directory
, 'docutils.conf'))
193 frontend
.make_paths_absolute(
194 local_config
, publisher
.option_parser
.relative_path_settings
,
196 settings
.update(local_config
, publisher
.option_parser
)
197 settings
.update(self
.settings_spec
.__dict
__, publisher
.option_parser
)
200 def run(self
, directory
=None, recurse
=1):
201 recurse
= recurse
and self
.initial_settings
.recurse
203 self
.directories
= [directory
]
204 elif self
.settings_spec
._directories
:
205 self
.directories
= self
.settings_spec
._directories
207 self
.directories
= [os
.getcwd()]
208 for directory
in self
.directories
:
209 for root
, dirs
, files
in os
.walk(directory
):
210 # os.walk by default this recurses down the tree,
211 # influence by modifying dirs.
214 self
.visit(root
, files
, dirs
)
216 def visit(self
, directory
, names
, subdirectories
):
217 settings
= self
.get_settings('', directory
)
218 errout
= ErrorOutput(encoding
=settings
.error_encoding
)
219 if settings
.prune
and (os
.path
.abspath(directory
) in settings
.prune
):
220 errout
.write('/// ...Skipping directory (pruned): %s\n' %
223 del subdirectories
[:]
225 if not self
.initial_settings
.silent
:
226 errout
.write('/// Processing directory: %s\n' % directory
)
228 # settings.ignore grows many duplicate entries as we recurse
229 # if we add patterns in config files or on the command line.
230 for pattern
in utils
.uniq(settings
.ignore
):
231 for i
in range(len(names
) - 1, -1, -1):
232 if fnmatch(names
[i
], pattern
):
236 if name
.endswith('.txt'):
237 self
.process_txt(directory
, name
)
239 def process_txt(self
, directory
, name
):
240 if name
.startswith('pep-'):
243 publisher
= self
.initial_settings
.writer
244 settings
= self
.get_settings(publisher
, directory
)
245 errout
= ErrorOutput(encoding
=settings
.error_encoding
)
246 pub_struct
= self
.publishers
[publisher
]
247 settings
._source
= os
.path
.normpath(os
.path
.join(directory
, name
))
248 settings
._destination
= settings
._source
[:-4]+'.html'
249 if not self
.initial_settings
.silent
:
250 errout
.write(' ::: Processing: %s\n' % name
)
253 if not settings
.dry_run
:
254 core
.publish_file(source_path
=settings
._source
,
255 destination_path
=settings
._destination
,
256 reader_name
=pub_struct
.reader_name
,
257 parser_name
='restructuredtext',
258 writer_name
=pub_struct
.writer_name
,
260 except ApplicationError
:
261 error
= sys
.exc_info()[1] # get exception in Python 3.x
262 errout
.write(' %s\n' % ErrorString(error
))
265 if __name__
== "__main__":