1#!/usr/bin/env python 2 3# $Id: buildhtml.py 8643 2021-03-26 13:51:21Z milde $ 4# Author: David Goodger <goodger@python.org> 5# Copyright: This module has been placed in the public domain. 6 7""" 8Generates .html from all the .txt files in a directory. 9 10Ordinary .txt files are understood to be standalone reStructuredText. 11Files named ``pep-*.txt`` are interpreted as reStructuredText PEPs. 12""" 13# Once PySource is here, build .html from .py as well. 14 15__docformat__ = 'reStructuredText' 16 17 18try: 19 import locale 20 locale.setlocale(locale.LC_ALL, '') 21except: 22 pass 23 24import sys 25import os 26import os.path 27import copy 28from fnmatch import fnmatch 29import docutils 30from docutils import ApplicationError 31from docutils import core, frontend, utils 32from docutils.utils.error_reporting import ErrorOutput, ErrorString 33from docutils.parsers import rst 34from docutils.readers import standalone, pep 35from docutils.writers import html4css1, html5_polyglot, pep_html 36 37 38usage = '%prog [options] [<directory> ...]' 39description = ('Generates .html from all the reStructuredText .txt files ' 40 '(including PEPs) in each <directory> ' 41 '(default is the current directory).') 42 43 44class SettingsSpec(docutils.SettingsSpec): 45 46 """ 47 Runtime settings & command-line options for the front end. 48 """ 49 50 prune_default = ['.hg', '.bzr', '.git', '.svn', 'CVS'] 51 52 # Can't be included in OptionParser below because we don't want to 53 # override the base class. 54 settings_spec = ( 55 'Build-HTML Options', 56 None, 57 (('Recursively scan subdirectories for files to process. This is ' 58 'the default.', 59 ['--recurse'], 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), 68 ['--prune'], 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).', 74 ['--ignore'], 75 {'metavar': '<patterns>', 'action': 'append', 76 'default': [], 77 'validator': frontend.validate_colon_separated_string_list}), 78 ('Docutils writer, one of "html", "html4", "html5". ' 79 'Default: "html" (use Docutils\' default HTML writer).', 80 ['--writer'], 81 {'metavar': '<writer>', 82 'choices': ['html', 'html4', 'html5'], 83 'default': 'html'}), 84 ('Obsoleted by "--writer".', 85 ['--html-writer'], 86 {'dest': 'writer', 87 'metavar': '<writer>', 88 'choices': ['html', 'html4', 'html5'],}), 89 ('Work silently (no progress messages). Independent of "--quiet".', 90 ['--silent'], 91 {'action': 'store_true', 'validator': frontend.validate_boolean}), 92 ('Do not process files, show files that would be processed.', 93 ['--dry-run'], 94 {'action': 'store_true', 'validator': frontend.validate_boolean}),)) 95 96 relative_path_settings = ('prune',) 97 config_section = 'buildhtml application' 98 config_section_dependencies = ('applications',) 99 100 101class OptionParser(frontend.OptionParser): 102 103 """ 104 Command-line option processing for the ``buildhtml.py`` front end. 105 """ 106 107 def check_values(self, values, args): 108 frontend.OptionParser.check_values(self, values, args) 109 values._source = None 110 return values 111 112 def check_args(self, args): 113 source = destination = None 114 if args: 115 self.values._directories = args 116 else: 117 self.values._directories = [os.getcwd()] 118 return source, destination 119 120 121class Struct(object): 122 123 """Stores data attributes for dotted-attribute access.""" 124 125 def __init__(self, **keywordargs): 126 self.__dict__.update(keywordargs) 127 128 129class Builder(object): 130 131 def __init__(self): 132 self.publishers = { 133 '': Struct(components=(pep.Reader, rst.Parser, pep_html.Writer, 134 SettingsSpec)), 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), 145 reader_name='pep', 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.""" 150 151 self.setup_publishers() 152 # default html writer (may change to html5 some time): 153 self.publishers['html'] = self.publishers['html4'] 154 155 def setup_publishers(self): 156 """ 157 Manage configurations for individual publishers. 158 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()`. 164 """ 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('') 178 179 def get_settings(self, publisher_name, directory=None): 180 """ 181 Return a settings object, from multiple sources. 182 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. 186 """ 187 publisher = self.publishers[publisher_name] 188 settings = frontend.Values(publisher.setting_defaults.__dict__) 189 settings.update(publisher.config_settings, publisher.option_parser) 190 if directory: 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, 195 directory) 196 settings.update(local_config, publisher.option_parser) 197 settings.update(self.settings_spec.__dict__, publisher.option_parser) 198 return settings 199 200 def run(self, directory=None, recurse=1): 201 recurse = recurse and self.initial_settings.recurse 202 if directory: 203 self.directories = [directory] 204 elif self.settings_spec._directories: 205 self.directories = self.settings_spec._directories 206 else: 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. 212 if not recurse: 213 del dirs[:] 214 self.visit(root, files, dirs) 215 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' % 221 directory) 222 sys.stderr.flush() 223 del subdirectories[:] 224 return 225 if not self.initial_settings.silent: 226 errout.write('/// Processing directory: %s\n' % directory) 227 sys.stderr.flush() 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): 233 # Modify in place! 234 del names[i] 235 for name in names: 236 if name.endswith('.txt'): 237 self.process_txt(directory, name) 238 239 def process_txt(self, directory, name): 240 if name.startswith('pep-'): 241 publisher = 'PEPs' 242 else: 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) 251 sys.stderr.flush() 252 try: 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, 259 settings=settings) 260 except ApplicationError: 261 error = sys.exc_info()[1] # get exception in Python 3.x 262 errout.write(' %s\n' % ErrorString(error)) 263 264 265if __name__ == "__main__": 266 Builder().run() 267