def publish(writer_name): reader=None reader_name='standalone' parser=None parser_name='restructuredtext' writer=None settings=None settings_spec=None settings_overrides=options[writer_name] config_section=None enable_exit=1 argv=[] usage=default_usage pub = Publisher(reader, parser, writer, settings=settings) pub.set_components(reader_name, parser_name, writer_name) settings = pub.get_settings(settings_spec=settings_spec, config_section=config_section) if settings_overrides: settings._update(settings_overrides, 'loose') source = file(source_path) pub.set_source(source, source_path) destination_path = 'pg1.' + extensions[writer_name] destination = file(destination_path, 'w') pub.set_destination(destination, destination_path) pub.publish(argv, usage, description, settings_spec, settings_overrides, config_section=config_section, enable_exit=enable_exit)
def main(args): argv = None reader = standalone.Reader() reader_name = 'standalone' writer = EpubWriter() writer_name = 'epub2' parser = Parser() parser_name = 'restructuredtext' settings = None settings_spec = None settings_overrides = None config_section = None enable_exit_status = 1 usage = default_usage publisher = Publisher(reader, parser, writer, settings, destination_class=EpubFileOutput) publisher.set_components(reader_name, parser_name, writer_name) description = ('Generates epub books from reStructuredText sources. ' + default_description) output = publisher.publish(argv, usage, description, settings_spec, settings_overrides, config_section=config_section, enable_exit_status=enable_exit_status)
def publish_cmdline_to_binary(reader=None, reader_name='standalone', parser=None, parser_name='restructuredtext', writer=None, writer_name='pseudoxml', settings=None, settings_spec=None, settings_overrides=None, config_section=None, enable_exit_status=1, argv=None, usage=default_usage, description=default_description, destination=None, destination_class=BinaryFileOutput ): """ Set up & run a `Publisher` for command-line-based file I/O (input and output file paths taken automatically from the command line). Return the encoded string output also. This is just like publish_cmdline, except that it uses io.BinaryFileOutput instead of io.FileOutput. Parameters: see `publish_programmatically` for the remainder. - `argv`: Command-line argument list to use instead of ``sys.argv[1:]``. - `usage`: Usage string, output if there's a problem parsing the command line. - `description`: Program description, output for the "--help" option (along with command-line option descriptions). """ pub = Publisher(reader, parser, writer, settings=settings, destination_class=destination_class) pub.set_components(reader_name, parser_name, writer_name) output = pub.publish( argv, usage, description, settings_spec, settings_overrides, config_section=config_section, enable_exit_status=enable_exit_status) return output
def publish_cmdline(reader=None, reader_name='standalone', parser=None, parser_name='restructuredtext', writer=None, writer_name='pseudoxml', settings=None, settings_spec=None, settings_overrides=None, config_section=None, enable_exit_status=1, argv=None, usage=default_usage, description=default_description): """ See docutils.core.publish_cmdline. We just modified this function to return the parsed destination file. """ pub = Publisher(reader, parser, writer, settings=settings) pub.set_components(reader_name, parser_name, writer_name) output = pub.publish(argv, usage, description, settings_spec, settings_overrides, config_section=config_section, enable_exit_status=enable_exit_status) return output, pub.settings._source, pub.settings._destination
def main(prog_args): argv = None reader = standalone.Reader() reader_name = 'standalone' writer = Writer() writer_name = 'pseudoxml' parser = None parser_name = 'restructuredtext' settings = None settings_spec = None settings_overrides = None config_section = None enable_exit_status = 1 usage = default_usage publisher = Publisher(reader, parser, writer, settings, destination_class=BinaryFileOutput) publisher.set_components(reader_name, parser_name, writer_name) description = ('Generates OpenDocument/OpenOffice/ODF slides from ' 'standalone reStructuredText sources. ' + default_description) output = publisher.publish(argv, usage, description, settings_spec, settings_overrides, config_section=config_section, enable_exit_status=enable_exit_status)
def main(args=sys.argv): argv = None reader = standalone.Reader() reader_name = "standalone" writer = EpubWriter() writer_name = "epub2" parser = Parser() parser_name = "restructuredtext" settings = None settings_spec = None settings_overrides = None config_section = None enable_exit_status = 1 usage = default_usage publisher = Publisher( reader, parser, writer, settings, destination_class=EpubFileOutput ) publisher.set_components(reader_name, parser_name, writer_name) description = ( "Generates epub books from reStructuredText sources. " + default_description ) publisher.publish( argv, usage, description, settings_spec, settings_overrides, config_section=config_section, enable_exit_status=enable_exit_status, )
def read_doc(app, env, filename): # type: (Sphinx, BuildEnvironment, unicode) -> nodes.document """Parse a document and convert to doctree.""" filetype = get_filetype(app.config.source_suffix, filename) input_class = app.registry.get_source_input(filetype) reader = SphinxStandaloneReader(app) source = input_class(app, env, source=None, source_path=filename, encoding=env.config.source_encoding) parser = app.registry.create_source_parser(app, filetype) if parser.__class__.__name__ == 'CommonMarkParser' and parser.settings_spec == (): # a workaround for recommonmark # If recommonmark.AutoStrictify is enabled, the parser invokes reST parser # internally. But recommonmark-0.4.0 does not provide settings_spec for reST # parser. As a workaround, this copies settings_spec for RSTParser to the # CommonMarkParser. parser.settings_spec = RSTParser.settings_spec pub = Publisher(reader=reader, parser=parser, writer=SphinxDummyWriter(), source_class=SphinxDummySourceClass, destination=NullOutput()) pub.set_components(None, 'restructuredtext', None) pub.process_programmatic_settings(None, env.settings, None) pub.set_source(source, filename) pub.publish() return pub.document
def get_sphinx(): sphinx = getattr(local_data, 'sphinx', None) if sphinx is None: sphinx = Sphinx(tempdir, tempdir, tempdir, tempdir, 'json', status=None, warning=None) sphinx.builder.translator_class = CustomHTMLTranslator sphinx.env.patch_lookup_functions() sphinx.env.temp_data['docname'] = 'text' sphinx.env.temp_data['default_domain'] = 'py' pub = Publisher(reader=None, parser=None, writer=HTMLWriter(sphinx.builder), source_class=io.StringInput, destination_class=io.NullOutput) pub.set_components('standalone', 'restructuredtext', None) pub.process_programmatic_settings(None, sphinx.env.settings, None) pub.set_destination(None, None) sphinx.publisher = pub local_data.sphinx = sphinx return sphinx, sphinx.publisher
def _to_odp_content(self, rst, xml_filename, odp_name='/tmp/out'): reader = standalone.Reader() reader_name = 'standalone' writer = rst2odp.Writer() writer_name = 'pseudoxml' parser = None parser_name = 'restructuredtext' settings = None settings_spec = None settings_overrides = None config_section = None enable_exit_status = 1 usage = default_usage publisher = Publisher(reader, parser, writer,# source=StringIO(rst), settings=settings, destination_class=rst2odp.BinaryFileOutput) publisher.set_components(reader_name, parser_name, writer_name) description = ('Generates OpenDocument/OpenOffice/ODF slides from ' 'standalone reStructuredText sources. ' + default_description) fin = open('/tmp/in.rst', 'w') fin.write(rst) fin.close() argv = ['--traceback', '/tmp/in.rst', odp_name] output = publisher.publish(argv, usage, description, settings_spec, settings_overrides, config_section=config_section, enable_exit_status=enable_exit_status) # pull content.xml out of /tmp/out z = zipwrap.Zippier(odp_name) fout = open(xml_filename, 'w') content = preso.pretty_xml(z.cat('content.xml')) fout.write(content) fout.close() return content
def init_publisher(): from docutils.core import Publisher from docutils.io import StringOutput p = Publisher(destination_class=StringOutput,writer=g_writer) p.get_settings() p.set_components('standalone', 'restructuredtext', 'html') p.set_destination(None, None) return p
def __init__(self): # publisher is used for html generation pub = Publisher(None, None, None, settings=None, source_class=StringInput, destination_class=StringOutput) pub.set_components('standalone', 'restructuredtext', 'html') pub.process_programmatic_settings(None, None, None) pub.set_destination(None, None) self.pub = pub
def run(self): log.debug("[%s.%s] Compiling manual from RST sources." % (__name__, self.__class__.__name__)) pub = Publisher(writer=manpage.Writer()) pub.set_components(reader_name='standalone', parser_name='restructuredtext', writer_name='pseudoxml') pub.publish(argv=[ u'%s' % get_path([DOCDIR, 'man', 'tribus.rst']), u'%s' % get_path([DOCDIR, 'man', 'tribus.1']) ])
def get_html(self, body_only=True, content_only=False, noclasses=False): import sys import pygments_rest from docutils.core import Publisher from docutils.io import StringInput, StringOutput from cStringIO import StringIO settings = { 'doctitle_xform': 1, 'pep_references': 1, 'rfc_references': 1, 'footnote_references': 'superscript', 'output_encoding': 'unicode', 'report_level': 2, # 2=show warnings, 3=show only errors, 5=off (docutils.utils } if content_only: post_rst = self.get_rst(noclasses=noclasses) else: post_rst = render_to('post_single.rst', post=self, noclasses=noclasses) pub = Publisher(reader=None, parser=None, writer=None, settings=None, source_class=StringInput, destination_class=StringOutput) pub.set_components(reader_name='standalone', parser_name='restructuredtext', writer_name='html') pub.process_programmatic_settings(settings_spec=None, settings_overrides=settings, config_section=None) pub.set_source(post_rst, source_path=self.module_path) pub.set_destination(None, None) errors_io = StringIO() real_stderr = sys.stderr sys.stderr = errors_io try: html_full = pub.publish(enable_exit_status=False) html_body = ''.join(pub.writer.html_body) finally: sys.stderr = real_stderr errors = errors_io.getvalue() self._process_rest_errors(errors) errors_io.close() return html_body if body_only else html_full
def convert(infilename, outfilename): print "converting %s to %s" % (infilename, outfilename) pub = Publisher() pub.set_components('standalone', # reader 'restructuredtext', # parser 'latex') # writer (arg, will be discarded) pub.reader = StandaloneReader() pub.writer = VerseLaTeXWriter() pub.process_programmatic_settings(None, None, None) pub.set_source(source_path=infilename) pub.set_destination(destination_path=outfilename) pub.publish()
def get_html(self, body_only=True, content_only=False, noclasses=False): import sys import pygments_rest from docutils.core import Publisher from docutils.io import StringInput, StringOutput from cStringIO import StringIO settings = {'doctitle_xform' : 1, 'pep_references' : 1, 'rfc_references' : 1, 'footnote_references': 'superscript', 'output_encoding' : 'unicode', 'report_level' : 2, # 2=show warnings, 3=show only errors, 5=off (docutils.utils } if content_only: post_rst = self.get_rst(noclasses=noclasses) else: post_rst = render_to('post_single.rst', post=self, noclasses=noclasses) pub = Publisher(reader=None, parser=None, writer=None, settings=None, source_class=StringInput, destination_class=StringOutput) pub.set_components(reader_name='standalone', parser_name='restructuredtext', writer_name='html') pub.process_programmatic_settings(settings_spec=None, settings_overrides=settings, config_section=None) pub.set_source(post_rst,source_path=self.module_path) pub.set_destination(None, None) errors_io = StringIO() real_stderr = sys.stderr sys.stderr = errors_io try: html_full = pub.publish(enable_exit_status=False) html_body = ''.join(pub.writer.html_body) finally: sys.stderr = real_stderr errors = errors_io.getvalue() self._process_rest_errors(errors) errors_io.close() return html_body if body_only else html_full
def convert(infilename, outfilename): print "converting %s to %s" % (infilename, outfilename) pub = Publisher() pub.set_components( 'standalone', # reader 'restructuredtext', # parser 'latex') # writer (arg, will be discarded) pub.reader = OptikReader() pub.writer = PyLaTeXWriter() pub.process_programmatic_settings(None, None, None) pub.set_source(source_path=infilename) pub.set_destination(destination_path=outfilename) pub.publish()
def process_labels(site, logger, source, post): site.processing_labels = True pub = Publisher(reader=Reader(), parser=None, writer=None) pub.set_components(None, 'restructuredtext', 'html') # Reading the file will generate output/errors that we don't care about # at this stage. The report_level = 5 means no output pub.process_programmatic_settings( settings_spec=None, settings_overrides={'report_level': 5}, config_section=None, ) pub.set_source(None, source) pub.publish() document = pub.document site.processing_labels = False # Code based on Sphinx std domain for name, is_explicit in document.nametypes.items(): if not is_explicit: continue labelid = document.nameids[name] if labelid is None: continue node = document.ids[labelid] if node.tagname == 'target' and 'refid' in node: node = document.ids.get(node['refid']) labelid = node['names'][0] if node.tagname == 'footnote' or 'refuri' in node or node.tagname.startswith( 'desc_'): continue if name in site.ref_labels: logger.warn( 'Duplicate label {dup}, other instance in {other}'.format( dup=name, other=site.ref_labels[name][0])) site.anon_ref_labels[name] = post.permalink(), labelid def clean_astext(node): """Like node.astext(), but ignore images. Taken from sphinx.util.nodes""" node = node.deepcopy() for img in node.traverse(nodes.image): img['alt'] = '' for raw in node.traverse(nodes.raw): raw.parent.remove(raw) return node.astext() if node.tagname in ('section', 'rubric'): sectname = clean_astext(node[0]) else: continue site.ref_labels[name] = post.permalink(), labelid, sectname
class HTMLGenerator: """ Really simple HTMLGenerator starting from publish_parts It reuses the docutils.core.Publisher class, which means it is *not* threadsafe. """ def __init__(self, settings_spec=None, settings_overrides=None, config_section='general'): if settings_overrides is None: settings_overrides = {'report_level': 5, 'halt_level': 5} self.pub = Publisher(reader=None, parser=None, writer=None, settings=None, source_class=io.StringInput, destination_class=io.StringOutput) self.pub.set_components(reader_name='standalone', parser_name='restructuredtext', writer_name='html') # hack: JEP-0071 does not allow HTML char entities, so we hack our way # out of it. # — == u"\u2014" # a setting to only emit charater entities in the writer would be nice # FIXME: several are emitted, and they are explicitly forbidden # in the JEP # == u"\u00a0" self.pub.writer.translator_class.attribution_formats['dash'] = ( '\u2014', '') self.pub.process_programmatic_settings(settings_spec, settings_overrides, config_section) def create_xhtml(self, text, destination=None, destination_path=None, enable_exit_status=None): """ Create xhtml for a fragment of IM dialog. We can use the source_name to store info about the message """ self.pub.set_source(text, None) self.pub.set_destination(destination, destination_path) output = self.pub.publish(enable_exit_status=enable_exit_status) # kludge until we can get docutils to stop generating (rare) # entities return '\u00a0'.join( self.pub.writer.parts['fragment'].strip().split(' '))
def _get_publisher(self, source_path): extra_params = {'initial_header_level': '2', 'syntax_highlight': 'short', 'input_encoding': 'utf-8'} user_params = self.settings.get('DOCUTILS_SETTINGS') if user_params: extra_params.update(user_params) pub = Publisher(destination_class=StringOutput) pub.set_components('standalone', 'restructuredtext', 'html') pub.writer.translator_class = HTMLTranslator pub.process_programmatic_settings(None, extra_params, None) pub.set_source(source_path=source_path) pub.publish() return pub
def get_doctree(path): """ Obtain a Sphinx doctree from the RST file at ``path``. Performs no Releases-specific processing; this code would, ideally, be in Sphinx itself, but things there are pretty tightly coupled. So we wrote this. :param str path: A relative or absolute file path string. :returns: A two-tuple of the generated ``sphinx.application.Sphinx`` app and the doctree (a ``docutils.document`` object). """ root, filename = os.path.split(path) docname, _ = os.path.splitext(filename) # TODO: this only works for top level changelog files (i.e. ones where # their dirname is the project/doc root) app = make_app(srcdir=root) # Create & init a BuildEnvironment. Mm, tasty side effects. app._init_env(freshenv=True) env = app.env env.update(config=app.config, srcdir=root, doctreedir=app.doctreedir, app=app) # Code taken from sphinx.environment.read_doc; easier to manually call # it with a working Environment object, instead of doing more random crap # to trick the higher up build system into thinking our single changelog # document was "updated". env.temp_data['docname'] = docname env.app = app # NOTE: SphinxStandaloneReader API changed in 1.4 :( reader_kwargs = {'app': env.app, 'parsers': env.config.source_parsers} if sphinx.version_info[:2] < (1, 4): del reader_kwargs['app'] reader = SphinxStandaloneReader(**reader_kwargs) pub = Publisher(reader=reader, writer=SphinxDummyWriter(), destination_class=NullOutput) pub.set_components(None, 'restructuredtext', None) pub.process_programmatic_settings(None, env.settings, None) # NOTE: docname derived higher up, from our given path src_path = env.doc2path(docname) source = SphinxFileInput(app, env, source=None, source_path=src_path, encoding=env.config.source_encoding) pub.source = source pub.settings._source = src_path pub.set_destination(None, None) pub.publish() return app, pub.document
def read_doc(app, env, filename): # type: (Sphinx, BuildEnvironment, unicode) -> nodes.document """Parse a document and convert to doctree.""" reader = SphinxStandaloneReader(app, parsers=app.registry.get_source_parsers()) source = SphinxFileInput(app, env, source=None, source_path=filename, encoding=env.config.source_encoding) pub = Publisher(reader=reader, writer=SphinxDummyWriter(), source_class=SphinxDummySourceClass, destination=NullOutput()) pub.set_components(None, 'restructuredtext', None) pub.process_programmatic_settings(None, env.settings, None) pub.set_source(source, filename) pub.publish() return pub.document
def check_rst(self, rst): pub = Publisher(reader=None, parser=None, writer=None, settings=None, source_class=io.StringInput, destination_class=io.StringOutput) pub.set_components(reader_name='standalone', parser_name='restructuredtext', writer_name='pseudoxml') pub.process_programmatic_settings( settings_spec=None, settings_overrides={'output_encoding': 'unicode'}, config_section=None, ) pub.set_source(rst, source_path=None) pub.set_destination(destination=None, destination_path=None) output = pub.publish(enable_exit_status=False) self.assertLess(pub.document.reporter.max_level, 0) return output, pub
def _get_publisher(self, source_path): extra_params = { 'initial_header_level': '2', 'syntax_highlight': 'short', 'input_encoding': 'utf-8' } user_params = self.settings.get('DOCUTILS_SETTINGS') if user_params: extra_params.update(user_params) pub = Publisher(destination_class=StringOutput) pub.set_components('standalone', 'restructuredtext', 'html') pub.writer.translator_class = HTMLTranslator pub.process_programmatic_settings(None, extra_params, None) pub.set_source(source_path=source_path) pub.publish() return pub
def render_map(builder, node): if node: doc = new_document(b('<partial node>')) doc.append(node) publisher = Publisher( source_class = DocTreeInput, destination_class=StringOutput) publisher.set_components('standalone', 'restructuredtext', 'pseudoxml') publisher.reader = DoctreeReader() publisher.writer = DitaMapWriter(builder) publisher.process_programmatic_settings(None, {'output_encoding': 'utf-8'}, None) publisher.set_source(doc, None) publisher.set_destination(None, None) publisher.publish() return publisher.writer.output output = XML_HEAD output += u"<map></map>" return output
def _get_publisher(self, source_path): extra_params = { "initial_header_level": "2", "syntax_highlight": "short", "input_encoding": "utf-8", } user_params = self.settings.get("DOCUTILS_SETTINGS") if user_params: extra_params.update(user_params) pub = Publisher(destination_class=StringOutput) pub.set_components("standalone", "restructuredtext", "html") pub.writer.translator_class = HTMLTranslator pub.process_programmatic_settings(None, extra_params, None) pub.set_source(source_path=source_path) pub.publish() return pub
class HTMLGenerator: '''Really simple HTMLGenerator starting from publish_parts. It reuses the docutils.core.Publisher class, which means it is *not* threadsafe. ''' def __init__(self, settings_spec=None, settings_overrides=dict(report_level=5, halt_level=5), config_section='general'): self.pub = Publisher(reader=None, parser=None, writer=None, settings=None, source_class=io.StringInput, destination_class=io.StringOutput) self.pub.set_components(reader_name='standalone', parser_name='restructuredtext', writer_name='html') # hack: JEP-0071 does not allow HTML char entities, so we hack our way # out of it. # — == u"\u2014" # a setting to only emit charater entities in the writer would be nice # FIXME: several are emitted, and they are explicitly forbidden # in the JEP # == u"\u00a0" self.pub.writer.translator_class.attribution_formats['dash'] = ( u'\u2014', '') self.pub.process_programmatic_settings(settings_spec, settings_overrides, config_section) def create_xhtml(self, text, destination=None, destination_path=None, enable_exit_status=None): ''' Create xhtml for a fragment of IM dialog. We can use the source_name to store info about the message.''' self.pub.set_source(text, None) self.pub.set_destination(destination, destination_path) output = self.pub.publish(enable_exit_status=enable_exit_status) # kludge until we can get docutils to stop generating (rare) # entities return u'\u00a0'.join(self.pub.writer.parts['fragment'].strip().split( ' '))
def publish_string_with_traceback(reader=None,reader_name=None, parser_name=None,writer_name=None, source=None,source_path=None): """A modified version of publish_string, so I can request traceback. """ from docutils.core import Publisher from docutils import io pub = Publisher(reader=reader, source_class=io.StringInput, destination_class=io.StringOutput) pub.set_components(reader_name="python", parser_name="restructuredtext", writer_name="pseudoxml") pub.process_command_line(argv=["--traceback"]) pub.set_source(source=source, source_path=source_path) return pub.publish(enable_exit=False)
def publish_cmdline(reader=None, reader_name='standalone', parser=None, parser_name='restructuredtext', writer=None, writer_name='pseudoxml', settings=None, settings_spec=None, settings_overrides=None, config_section=None, enable_exit_status=1, argv=None, usage=default_usage, description=default_description): """ See docutils.core.publish_cmdline. We just modified this function to return the parsed destination file. """ pub = Publisher(reader, parser, writer, settings=settings) pub.set_components(reader_name, parser_name, writer_name) output = pub.publish( argv, usage, description, settings_spec, settings_overrides, config_section=config_section, enable_exit_status=enable_exit_status) return output, pub.settings._source, pub.settings._destination
def aoeuaoeuaoeu_docs(f): ''' I couldn't figure out how to use the docutils docstring parser, so I wrote my own. Can somebody show me the right way to do this? ''' raise NotImplementedError from docutils.core import Publisher from docutils.io import StringInput pub = Publisher(None, None, None, settings = settings, source_class = StringInput, destination_class = destination_class) pub.set_components('standalone', 'restructuredtext', 'pseudoxml') pub.process_programmatic_settings( settings_spec, settings_overrides, config_section) pub.set_source(f.__doc__, f.__name__) pub.set_destination(None, f.__name__) output = pub.publish(enable_exit_status = False) return output, pub return publish_parts(f.__doc__, source_class = StringInput, source_path = f.__name__)
def read_doc(app, env, filename): # type: (Sphinx, BuildEnvironment, unicode) -> nodes.document """Parse a document and convert to doctree.""" input_class = app.registry.get_source_input(filename) reader = SphinxStandaloneReader(app) source = input_class(app, env, source=None, source_path=filename, encoding=env.config.source_encoding) parser = app.registry.create_source_parser(app, filename) pub = Publisher(reader=reader, parser=parser, writer=SphinxDummyWriter(), source_class=SphinxDummySourceClass, destination=NullOutput()) pub.set_components(None, 'restructuredtext', None) pub.process_programmatic_settings(None, env.settings, None) pub.set_source(source, filename) pub.publish() return pub.document
def _to_odp_content(self, rst, xml_filename, odp_name='/tmp/out'): reader = standalone.Reader() reader_name = 'standalone' writer = rst2odp.Writer() writer_name = 'pseudoxml' parser = None parser_name = 'restructuredtext' settings = None settings_spec = None settings_overrides = None config_section = None enable_exit_status = 1 usage = default_usage publisher = Publisher( reader, parser, writer, # source=StringIO(rst), settings=settings, destination_class=rst2odp.BinaryFileOutput) publisher.set_components(reader_name, parser_name, writer_name) description = ('Generates OpenDocument/OpenOffice/ODF slides from ' 'standalone reStructuredText sources. ' + default_description) fin = open('/tmp/in.rst', 'w') fin.write(rst) fin.close() argv = ['--traceback', '/tmp/in.rst', odp_name] output = publisher.publish(argv, usage, description, settings_spec, settings_overrides, config_section=config_section, enable_exit_status=enable_exit_status) # pull content.xml out of /tmp/out z = zipwrap.Zippier(odp_name) fout = open(xml_filename, 'w') content = preso.pretty_xml(z.cat('content.xml')) fout.write(content) fout.close() return content
def sandbox_partial_builder(doctree, env): env.process_downloads(env.docname, doctree) for domain in env.domains.values(): domain.process_doc(env, env.docname, doctree) env.resolve_references(doctree, env.docname, env.app.builder) if not hasattr(env.app.builder, 'dlpath'): # FIXME: builders.html.StandaloneHTMLBuilder.write_doc で設定される属性のため、この時点では存在しない env.app.builder.dlpath = '_downloads' env.app.builder.fignumbers = env.toc_secnumbers.get(env.docname, {}) env.app.builder.secnumbers = env.toc_secnumbers.get(env.docname, {}) pub = Publisher( reader=SandboxDoctreeReader(), writer=Writer(env.app.builder), source=DocTreeInput(doctree), destination_class=StringOutput, ) pub.set_components(None, 'restructuredtext', None) defaults = env.settings.copy() defaults['output_encoding'] = 'unicode' pub.process_programmatic_settings(None, defaults, None) pub.set_destination(None, None) out = pub.publish(enable_exit_status=False) return pub.writer.parts['fragment']
class _PydocParser: def __init__(self): # Set up the instance we'll be using to render docstrings. self.errors = [] self.writer = _DocumentPseudoWriter() self.publisher = Publisher(_EpydocReader(self.errors), writer=self.writer, source_class=io.StringInput) self.publisher.set_components('standalone', 'restructuredtext', 'pseudoxml') settings_overrides={ 'report_level':10000, 'halt_level':10000, 'warning_stream':None, } self.publisher.process_programmatic_settings(None, settings_overrides, None) self.publisher.set_destination() def parse_docstring(self, docstring, errors): """Parse a docstring for eventual transformation into HTML This function is a replacement for parse_docstring from epydoc.markup.restructuredtext.parse_docstring. This function reuses the Publisher instance while the original did not. Using This function yields significantly faster WADL generation for complex systems. """ # Clear any errors from previous calls. del self.errors[:] self.publisher.set_source(docstring, None) self.publisher.publish() # Move any errors into the caller-provided list. errors[:] = self.errors[:] return ParsedRstDocstring(self.writer.document)
def _get_publisher(self, source_path, translator_class=BlogHTMLTranslator): extra_params = { "initial_header_level": "2", "syntax_highlight": "short", "input_encoding": "utf-8", "exit_status_level": 2, "language_code": self._language_code, "halt_level": 2, "traceback": True, "warning_stream": StringIO(), "embed_stylesheet": False, } user_params = self.settings.get("DOCUTILS_SETTINGS") if user_params: extra_params.update(user_params) pub = Publisher(writer=self.writer_class(), destination_class=StringOutput) pub.set_components("standalone", "restructuredtext", "html") pub.writer.translator_class = translator_class pub.process_programmatic_settings(None, extra_params, None) pub.set_source(source_path=source_path) pub.publish() return pub
def main(args): print "ARGS", args argv = None reader = standalone.Reader() reader_name = 'standalone' writer = EpubWriter() writer_name = 'epub2' parser = Parser() parser_name = 'restructuredtext' settings = None settings_spec = None settings_overrides = None config_section = None enable_exit_status = 1 usage = default_usage publisher = Publisher(reader, parser, writer, settings, destination_class=EpubFileOutput) publisher.set_components(reader_name, parser_name, writer_name) description = ('Generates epub books from reStructuredText sources. ' + default_description) output = publisher.publish(argv, usage, description, settings_spec, settings_overrides, config_section=config_section, enable_exit_status=enable_exit_status)
def check_for_errors(content, filepath=None): """Lint reStructuredText and return errors :param string content: reStructuredText to be linted :param string filepath: Optional path to file, this will be returned as the source :rtype list: List of errors. Each error will contain a line, source (filepath), message (error message), and full message (error message + source lines) """ # Generate a new parser (copying `rst2html.py` flow) # http://repo.or.cz/w/docutils.git/blob/422cede485668203abc01c76ca317578ff634b30:/docutils/tools/rst2html.py # http://repo.or.cz/w/docutils.git/blob/422cede485668203abc01c76ca317578ff634b30:/docutils/docutils/core.py#l348 pub = Publisher(None, None, None, settings=None) pub.set_components('standalone', 'restructuredtext', 'pseudoxml') # Configure publisher # DEV: We cannot use `process_command_line` since it processes `sys.argv` which is for `rst-lint`, not `docutils` # http://repo.or.cz/w/docutils.git/blob/422cede485668203abc01c76ca317578ff634b30:/docutils/docutils/core.py#l201 # http://repo.or.cz/w/docutils.git/blob/422cede485668203abc01c76ca317578ff634b30:/docutils/docutils/core.py#l143 # http://repo.or.cz/w/docutils.git/blob/422cede485668203abc01c76ca317578ff634b30:/docutils/docutils/core.py#l118 settings = pub.get_settings(halt_level=5) pub.set_io() # Prepare a document to parse on # DEV: We avoid the `read` method because when `source` is `None`, it attempts to read from `stdin`. # However, we already know our content. # DEV: We create our document without `parse` because we need to attach observer's before parsing # http://repo.or.cz/w/docutils.git/blob/422cede485668203abc01c76ca317578ff634b30:/docutils/docutils/readers/__init__.py#l66 reader = pub.reader document = utils.new_document(filepath, settings) # Disable stdout # TODO: Find a more proper way to do this # TODO: We might exit the program if a certain error level is reached document.reporter.stream = None # Collect errors via an observer errors = [] def error_collector(data): # Mutate the data since it was just generated data.line = data.get('line') data.source = data['source'] data.level = data['level'] data.type = data['type'] data.message = Element.astext(data.children[0]) data.full_message = Element.astext(data) # Save the error errors.append(data) document.reporter.attach_observer(error_collector) # Parse the content (and collect errors) # http://repo.or.cz/w/docutils.git/blob/422cede485668203abc01c76ca317578ff634b30:/docutils/docutils/readers/__init__.py#l75 reader.parser.parse(content, document) # Apply transforms (and more collect errors) # DEV: We cannot use `apply_transforms` since it has `attach_observer` baked in. We want only our listener. # http://repo.or.cz/w/docutils.git/blob/422cede485668203abc01c76ca317578ff634b30:/docutils/docutils/core.py#l195 # http://repo.or.cz/w/docutils.git/blob/422cede485668203abc01c76ca317578ff634b30:/docutils/docutils/transforms/__init__.py#l159 document.transformer.populate_from_components( (pub.source, pub.reader, pub.reader.parser, pub.writer, pub.destination) ) transformer = document.transformer while transformer.transforms: if not transformer.sorted: # Unsorted initially, and whenever a transform is added. transformer.transforms.sort() transformer.transforms.reverse() transformer.sorted = 1 priority, transform_class, pending, kwargs = transformer.transforms.pop() transform = transform_class(transformer.document, startnode=pending) transform.apply(**kwargs) transformer.applied.append((priority, transform_class, pending, kwargs)) return errors
class StandaloneHTMLBuilder(Builder): """ Builds standalone HTML docs. """ name = "html" format = "html" copysource = True out_suffix = ".html" link_suffix = ".html" # defaults to matching out_suffix indexer_format = js_index supported_image_types = ["image/svg+xml", "image/png", "image/gif", "image/jpeg"] searchindex_filename = "searchindex.js" add_permalinks = True embedded = False # for things like HTML help or Qt help: suppresses sidebar # This is a class attribute because it is mutated by Sphinx.add_javascript. script_files = ["_static/jquery.js", "_static/underscore.js", "_static/doctools.js"] # Dito for this one. css_files = [] default_sidebars = ["localtoc.html", "relations.html", "sourcelink.html", "searchbox.html"] # cached publisher object for snippets _publisher = None def init(self): # a hash of all config values that, if changed, cause a full rebuild self.config_hash = "" self.tags_hash = "" # section numbers for headings in the currently visited document self.secnumbers = {} self.init_templates() self.init_highlighter() self.init_translator_class() if self.config.html_file_suffix is not None: self.out_suffix = self.config.html_file_suffix if self.config.html_link_suffix is not None: self.link_suffix = self.config.html_link_suffix else: self.link_suffix = self.out_suffix if self.config.language is not None: jsfile_list = [ path.join(package_dir, "locale", self.config.language, "LC_MESSAGES", "sphinx.js"), path.join(sys.prefix, "share/sphinx/locale", self.config.language, "sphinx.js"), ] for jsfile in jsfile_list: if path.isfile(jsfile): self.script_files.append("_static/translations.js") break def get_theme_config(self): return self.config.html_theme, self.config.html_theme_options def init_templates(self): Theme.init_themes(self) themename, themeoptions = self.get_theme_config() self.theme = Theme(themename) self.theme_options = themeoptions.copy() self.create_template_bridge() self.templates.init(self, self.theme) def init_highlighter(self): # determine Pygments style and create the highlighter if self.config.pygments_style is not None: style = self.config.pygments_style elif self.theme: style = self.theme.get_confstr("theme", "pygments_style", "none") else: style = "sphinx" self.highlighter = PygmentsBridge("html", style, self.config.trim_doctest_flags) def init_translator_class(self): if self.config.html_translator_class: self.translator_class = self.app.import_object( self.config.html_translator_class, "html_translator_class setting" ) elif self.config.html_use_smartypants: self.translator_class = SmartyPantsHTMLTranslator else: self.translator_class = HTMLTranslator def get_outdated_docs(self): cfgdict = dict( (name, self.config[name]) for (name, desc) in self.config.values.iteritems() if desc[1] == "html" ) self.config_hash = md5(str(cfgdict)).hexdigest() self.tags_hash = md5(str(sorted(self.tags))).hexdigest() old_config_hash = old_tags_hash = "" try: fp = open(path.join(self.outdir, ".buildinfo")) version = fp.readline() if version.rstrip() != "# Sphinx build info version 1": raise ValueError fp.readline() # skip commentary cfg, old_config_hash = fp.readline().strip().split(": ") if cfg != "config": raise ValueError tag, old_tags_hash = fp.readline().strip().split(": ") if tag != "tags": raise ValueError fp.close() except ValueError: self.warn("unsupported build info format in %r, building all" % path.join(self.outdir, ".buildinfo")) except Exception: pass if old_config_hash != self.config_hash or old_tags_hash != self.tags_hash: for docname in self.env.found_docs: yield docname return if self.templates: template_mtime = self.templates.newest_template_mtime() else: template_mtime = 0 for docname in self.env.found_docs: if docname not in self.env.all_docs: yield docname continue targetname = self.get_outfilename(docname) try: targetmtime = path.getmtime(targetname) except Exception: targetmtime = 0 try: srcmtime = max(path.getmtime(self.env.doc2path(docname)), template_mtime) if srcmtime > targetmtime: yield docname except EnvironmentError: # source doesn't exist anymore pass def render_partial(self, node): """Utility: Render a lone doctree node.""" if node is None: return {"fragment": ""} doc = new_document("<partial node>") doc.append(node) if self._publisher is None: self._publisher = Publisher(source_class=DocTreeInput, destination_class=StringOutput) self._publisher.set_components("standalone", "restructuredtext", "pseudoxml") pub = self._publisher pub.reader = DoctreeReader() pub.writer = HTMLWriter(self) pub.process_programmatic_settings(None, {"output_encoding": "unicode"}, None) pub.set_source(doc, None) pub.set_destination(None, None) pub.publish() return pub.writer.parts def prepare_writing(self, docnames): from sphinx.search import IndexBuilder self.indexer = IndexBuilder(self.env) self.load_indexer(docnames) self.docwriter = HTMLWriter(self) self.docsettings = OptionParser(defaults=self.env.settings, components=(self.docwriter,)).get_default_values() self.docsettings.compact_lists = bool(self.config.html_compact_lists) # determine the additional indices to include self.domain_indices = [] # html_domain_indices can be False/True or a list of index names indices_config = self.config.html_domain_indices if indices_config: for domain in self.env.domains.itervalues(): for indexcls in domain.indices: indexname = "%s-%s" % (domain.name, indexcls.name) if isinstance(indices_config, list): if indexname not in indices_config: continue # deprecated config value if indexname == "py-modindex" and not self.config.html_use_modindex: continue content, collapse = indexcls(domain).generate() if content: self.domain_indices.append((indexname, indexcls, content, collapse)) # format the "last updated on" string, only once is enough since it # typically doesn't include the time of day lufmt = self.config.html_last_updated_fmt if lufmt is not None: self.last_updated = ustrftime(lufmt or _("%b %d, %Y")) else: self.last_updated = None logo = self.config.html_logo and path.basename(self.config.html_logo) or "" favicon = self.config.html_favicon and path.basename(self.config.html_favicon) or "" if favicon and os.path.splitext(favicon)[1] != ".ico": self.warn("html_favicon is not an .ico file") if not isinstance(self.config.html_use_opensearch, basestring): self.warn("html_use_opensearch config value must now be a string") self.relations = self.env.collect_relations() rellinks = [] if self.config.html_use_index: rellinks.append(("genindex", _("General Index"), "I", _("index"))) for indexname, indexcls, content, collapse in self.domain_indices: # if it has a short name if indexcls.shortname: rellinks.append((indexname, indexcls.localname, "", indexcls.shortname)) if self.config.html_style is not None: stylename = self.config.html_style elif self.theme: stylename = self.theme.get_confstr("theme", "stylesheet") else: stylename = "default.css" self.globalcontext = dict( embedded=self.embedded, project=self.config.project, release=self.config.release, version=self.config.version, last_updated=self.last_updated, copyright=self.config.copyright, master_doc=self.config.master_doc, use_opensearch=self.config.html_use_opensearch, docstitle=self.config.html_title, shorttitle=self.config.html_short_title, show_copyright=self.config.html_show_copyright, show_sphinx=self.config.html_show_sphinx, has_source=self.config.html_copy_source, show_source=self.config.html_show_sourcelink, file_suffix=self.out_suffix, script_files=self.script_files, css_files=self.css_files, sphinx_version=__version__, style=stylename, rellinks=rellinks, builder=self.name, parents=[], logo=logo, favicon=favicon, ) if self.theme: self.globalcontext.update( ("theme_" + key, val) for (key, val) in self.theme.get_options(self.theme_options).iteritems() ) self.globalcontext.update(self.config.html_context) def get_doc_context(self, docname, body, metatags): """Collect items for the template context of a page.""" # find out relations prev = next = None parents = [] rellinks = self.globalcontext["rellinks"][:] related = self.relations.get(docname) titles = self.env.titles if related and related[2]: try: next = { "link": self.get_relative_uri(docname, related[2]), "title": self.render_partial(titles[related[2]])["title"], } rellinks.append((related[2], next["title"], "N", _("next"))) except KeyError: next = None if related and related[1]: try: prev = { "link": self.get_relative_uri(docname, related[1]), "title": self.render_partial(titles[related[1]])["title"], } rellinks.append((related[1], prev["title"], "P", _("previous"))) except KeyError: # the relation is (somehow) not in the TOC tree, handle # that gracefully prev = None while related and related[0]: try: parents.append( { "link": self.get_relative_uri(docname, related[0]), "title": self.render_partial(titles[related[0]])["title"], } ) except KeyError: pass related = self.relations.get(related[0]) if parents: parents.pop() # remove link to the master file; we have a generic # "back to index" link already parents.reverse() # title rendered as HTML title = self.env.longtitles.get(docname) title = title and self.render_partial(title)["title"] or "" # the name for the copied source sourcename = self.config.html_copy_source and docname + ".txt" or "" # metadata for the document meta = self.env.metadata.get(docname) # local TOC and global TOC tree toc = self.render_partial(self.env.get_toc_for(docname))["fragment"] return dict( parents=parents, prev=prev, next=next, title=title, meta=meta, body=body, metatags=metatags, rellinks=rellinks, sourcename=sourcename, toc=toc, # only display a TOC if there's more than one item to show display_toc=(self.env.toc_num_entries[docname] > 1), ) def write_doc(self, docname, doctree): destination = StringOutput(encoding="utf-8") doctree.settings = self.docsettings self.secnumbers = self.env.toc_secnumbers.get(docname, {}) self.imgpath = relative_uri(self.get_target_uri(docname), "_images") self.post_process_images(doctree) self.dlpath = relative_uri(self.get_target_uri(docname), "_downloads") self.docwriter.write(doctree, destination) self.docwriter.assemble_parts() body = self.docwriter.parts["fragment"] metatags = self.docwriter.clean_meta ctx = self.get_doc_context(docname, body, metatags) self.index_page(docname, doctree, ctx.get("title", "")) self.handle_page(docname, ctx, event_arg=doctree) def finish(self): self.info(bold("writing additional files..."), nonl=1) # pages from extensions for pagelist in self.app.emit("html-collect-pages"): for pagename, context, template in pagelist: self.handle_page(pagename, context, template) # the global general index if self.config.html_use_index: self.write_genindex() # the global domain-specific indices self.write_domain_indices() # the search page if self.name != "htmlhelp": self.info(" search", nonl=1) self.handle_page("search", {}, "search.html") # additional pages from conf.py for pagename, template in self.config.html_additional_pages.items(): self.info(" " + pagename, nonl=1) self.handle_page(pagename, {}, template) if self.config.html_use_opensearch and self.name != "htmlhelp": self.info(" opensearch", nonl=1) fn = path.join(self.outdir, "_static", "opensearch.xml") self.handle_page("opensearch", {}, "opensearch.xml", outfilename=fn) self.info() self.copy_image_files() self.copy_download_files() self.copy_static_files() self.write_buildinfo() # dump the search index self.handle_finish() def write_genindex(self): # the total count of lines for each index letter, used to distribute # the entries into two columns genindex = self.env.create_index(self) indexcounts = [] for _, entries in genindex: indexcounts.append(sum(1 + len(subitems) for _, (_, subitems) in entries)) genindexcontext = dict( genindexentries=genindex, genindexcounts=indexcounts, split_index=self.config.html_split_index ) self.info(" genindex", nonl=1) if self.config.html_split_index: self.handle_page("genindex", genindexcontext, "genindex-split.html") self.handle_page("genindex-all", genindexcontext, "genindex.html") for (key, entries), count in zip(genindex, indexcounts): ctx = {"key": key, "entries": entries, "count": count, "genindexentries": genindex} self.handle_page("genindex-" + key, ctx, "genindex-single.html") else: self.handle_page("genindex", genindexcontext, "genindex.html") def write_domain_indices(self): for indexname, indexcls, content, collapse in self.domain_indices: indexcontext = dict(indextitle=indexcls.localname, content=content, collapse_index=collapse) self.info(" " + indexname, nonl=1) self.handle_page(indexname, indexcontext, "domainindex.html") def copy_image_files(self): # copy image files if self.images: ensuredir(path.join(self.outdir, "_images")) for src in self.status_iterator(self.images, "copying images... ", brown, len(self.images)): dest = self.images[src] try: copyfile(path.join(self.srcdir, src), path.join(self.outdir, "_images", dest)) except Exception, err: self.warn("cannot copy image file %r: %s" % (path.join(self.srcdir, src), err))
class StandaloneHTMLBuilder(Builder): """ Builds standalone HTML docs. """ name = 'html' format = 'html' copysource = True allow_parallel = True out_suffix = '.html' link_suffix = '.html' # defaults to matching out_suffix indexer_format = js_index indexer_dumps_unicode = True supported_image_types = [ 'image/svg+xml', 'image/png', 'image/gif', 'image/jpeg' ] searchindex_filename = 'searchindex.js' add_permalinks = True embedded = False # for things like HTML help or Qt help: suppresses sidebar # This is a class attribute because it is mutated by Sphinx.add_javascript. script_files = [ '_static/jquery.js', '_static/underscore.js', '_static/doctools.js' ] # Dito for this one. css_files = [] default_sidebars = [ 'localtoc.html', 'relations.html', 'sourcelink.html', 'searchbox.html' ] # cached publisher object for snippets _publisher = None def init(self): # a hash of all config values that, if changed, cause a full rebuild self.config_hash = '' self.tags_hash = '' # section numbers for headings in the currently visited document self.secnumbers = {} # currently written docname self.current_docname = None self.init_templates() self.init_highlighter() self.init_translator_class() if self.config.html_file_suffix is not None: self.out_suffix = self.config.html_file_suffix if self.config.html_link_suffix is not None: self.link_suffix = self.config.html_link_suffix else: self.link_suffix = self.out_suffix if self.config.language is not None: if self._get_translations_js(): self.script_files.append('_static/translations.js') def _get_translations_js(self): candidates = [path.join(package_dir, 'locale', self.config.language, 'LC_MESSAGES', 'sphinx.js'), path.join(sys.prefix, 'share/sphinx/locale', self.config.language, 'sphinx.js')] + \ [path.join(dir, self.config.language, 'LC_MESSAGES', 'sphinx.js') for dir in self.config.locale_dirs] for jsfile in candidates: if path.isfile(jsfile): return jsfile return None def get_theme_config(self): return self.config.html_theme, self.config.html_theme_options def init_templates(self): Theme.init_themes(self.confdir, self.config.html_theme_path, warn=self.warn) themename, themeoptions = self.get_theme_config() self.theme = Theme(themename) self.theme_options = themeoptions.copy() self.create_template_bridge() self.templates.init(self, self.theme) def init_highlighter(self): # determine Pygments style and create the highlighter if self.config.pygments_style is not None: style = self.config.pygments_style elif self.theme: style = self.theme.get_confstr('theme', 'pygments_style', 'none') else: style = 'sphinx' self.highlighter = PygmentsBridge('html', style, self.config.trim_doctest_flags) def init_translator_class(self): if self.config.html_translator_class: self.translator_class = self.app.import_object( self.config.html_translator_class, 'html_translator_class setting') elif self.config.html_use_smartypants: self.translator_class = SmartyPantsHTMLTranslator else: self.translator_class = HTMLTranslator def get_outdated_docs(self): cfgdict = dict((name, self.config[name]) for (name, desc) in self.config.values.iteritems() if desc[1] == 'html') self.config_hash = get_stable_hash(cfgdict) self.tags_hash = get_stable_hash(sorted(self.tags)) old_config_hash = old_tags_hash = '' try: fp = open(path.join(self.outdir, '.buildinfo')) try: version = fp.readline() if version.rstrip() != '# Sphinx build info version 1': raise ValueError fp.readline() # skip commentary cfg, old_config_hash = fp.readline().strip().split(': ') if cfg != 'config': raise ValueError tag, old_tags_hash = fp.readline().strip().split(': ') if tag != 'tags': raise ValueError finally: fp.close() except ValueError: self.warn('unsupported build info format in %r, building all' % path.join(self.outdir, '.buildinfo')) except Exception: pass if old_config_hash != self.config_hash or \ old_tags_hash != self.tags_hash: for docname in self.env.found_docs: yield docname return if self.templates: template_mtime = self.templates.newest_template_mtime() else: template_mtime = 0 for docname in self.env.found_docs: if docname not in self.env.all_docs: yield docname continue targetname = self.get_outfilename(docname) try: targetmtime = path.getmtime(targetname) except Exception: targetmtime = 0 try: srcmtime = max(path.getmtime(self.env.doc2path(docname)), template_mtime) if srcmtime > targetmtime: yield docname except EnvironmentError: # source doesn't exist anymore pass def render_partial(self, node): """Utility: Render a lone doctree node.""" if node is None: return {'fragment': ''} doc = new_document(b('<partial node>')) doc.append(node) if self._publisher is None: self._publisher = Publisher(source_class=DocTreeInput, destination_class=StringOutput) self._publisher.set_components('standalone', 'restructuredtext', 'pseudoxml') pub = self._publisher pub.reader = DoctreeReader() pub.writer = HTMLWriter(self) pub.process_programmatic_settings(None, {'output_encoding': 'unicode'}, None) pub.set_source(doc, None) pub.set_destination(None, None) pub.publish() return pub.writer.parts def prepare_writing(self, docnames): # create the search indexer from sphinx.search import IndexBuilder, languages lang = self.config.html_search_language or self.config.language if not lang or lang not in languages: lang = 'en' self.indexer = IndexBuilder(self.env, lang, self.config.html_search_options, self.config.html_search_scorer) self.load_indexer(docnames) self.docwriter = HTMLWriter(self) self.docsettings = OptionParser( defaults=self.env.settings, components=(self.docwriter, )).get_default_values() self.docsettings.compact_lists = bool(self.config.html_compact_lists) # determine the additional indices to include self.domain_indices = [] # html_domain_indices can be False/True or a list of index names indices_config = self.config.html_domain_indices if indices_config: for domain in self.env.domains.itervalues(): for indexcls in domain.indices: indexname = '%s-%s' % (domain.name, indexcls.name) if isinstance(indices_config, list): if indexname not in indices_config: continue # deprecated config value if indexname == 'py-modindex' and \ not self.config.html_use_modindex: continue content, collapse = indexcls(domain).generate() if content: self.domain_indices.append( (indexname, indexcls, content, collapse)) # format the "last updated on" string, only once is enough since it # typically doesn't include the time of day lufmt = self.config.html_last_updated_fmt if lufmt is not None: self.last_updated = ustrftime(lufmt or _('%b %d, %Y')) else: self.last_updated = None logo = self.config.html_logo and \ path.basename(self.config.html_logo) or '' favicon = self.config.html_favicon and \ path.basename(self.config.html_favicon) or '' if favicon and os.path.splitext(favicon)[1] != '.ico': self.warn('html_favicon is not an .ico file') if not isinstance(self.config.html_use_opensearch, basestring): self.warn('html_use_opensearch config value must now be a string') self.relations = self.env.collect_relations() rellinks = [] if self.get_builder_config('use_index', 'html'): rellinks.append(('genindex', _('General Index'), 'I', _('index'))) for indexname, indexcls, content, collapse in self.domain_indices: # if it has a short name if indexcls.shortname: rellinks.append( (indexname, indexcls.localname, '', indexcls.shortname)) if self.config.html_style is not None: stylename = self.config.html_style elif self.theme: stylename = self.theme.get_confstr('theme', 'stylesheet') else: stylename = 'default.css' self.globalcontext = dict( embedded=self.embedded, project=self.config.project, release=self.config.release, version=self.config.version, last_updated=self.last_updated, copyright=self.config.copyright, master_doc=self.config.master_doc, use_opensearch=self.config.html_use_opensearch, docstitle=self.config.html_title, shorttitle=self.config.html_short_title, show_copyright=self.config.html_show_copyright, show_sphinx=self.config.html_show_sphinx, has_source=self.config.html_copy_source, show_source=self.config.html_show_sourcelink, file_suffix=self.out_suffix, script_files=self.script_files, css_files=self.css_files, sphinx_version=__version__, style=stylename, rellinks=rellinks, builder=self.name, parents=[], logo=logo, favicon=favicon, ) if self.theme: self.globalcontext.update(('theme_' + key, val) for ( key, val) in self.theme.get_options(self.theme_options).iteritems()) self.globalcontext.update(self.config.html_context) def get_doc_context(self, docname, body, metatags): """Collect items for the template context of a page.""" # find out relations prev = next = None parents = [] rellinks = self.globalcontext['rellinks'][:] related = self.relations.get(docname) titles = self.env.titles if related and related[2]: try: next = { 'link': self.get_relative_uri(docname, related[2]), 'title': self.render_partial(titles[related[2]])['title'] } rellinks.append((related[2], next['title'], 'N', _('next'))) except KeyError: next = None if related and related[1]: try: prev = { 'link': self.get_relative_uri(docname, related[1]), 'title': self.render_partial(titles[related[1]])['title'] } rellinks.append( (related[1], prev['title'], 'P', _('previous'))) except KeyError: # the relation is (somehow) not in the TOC tree, handle # that gracefully prev = None while related and related[0]: try: parents.append({ 'link': self.get_relative_uri(docname, related[0]), 'title': self.render_partial(titles[related[0]])['title'] }) except KeyError: pass related = self.relations.get(related[0]) if parents: parents.pop() # remove link to the master file; we have a generic # "back to index" link already parents.reverse() # title rendered as HTML title = self.env.longtitles.get(docname) title = title and self.render_partial(title)['title'] or '' # the name for the copied source sourcename = self.config.html_copy_source and docname + '.txt' or '' # metadata for the document meta = self.env.metadata.get(docname) # local TOC and global TOC tree self_toc = self.env.get_toc_for(docname, self) toc = self.render_partial(self_toc)['fragment'] return dict( parents=parents, prev=prev, next=next, title=title, meta=meta, body=body, metatags=metatags, rellinks=rellinks, sourcename=sourcename, toc=toc, # only display a TOC if there's more than one item to show display_toc=(self.env.toc_num_entries[docname] > 1), ) def write_doc(self, docname, doctree): destination = StringOutput(encoding='utf-8') doctree.settings = self.docsettings self.secnumbers = self.env.toc_secnumbers.get(docname, {}) self.imgpath = relative_uri(self.get_target_uri(docname), '_images') self.dlpath = relative_uri(self.get_target_uri(docname), '_downloads') self.current_docname = docname self.docwriter.write(doctree, destination) self.docwriter.assemble_parts() body = self.docwriter.parts['fragment'] metatags = self.docwriter.clean_meta ctx = self.get_doc_context(docname, body, metatags) self.handle_page(docname, ctx, event_arg=doctree) def write_doc_serialized(self, docname, doctree): self.imgpath = relative_uri(self.get_target_uri(docname), '_images') self.post_process_images(doctree) title = self.env.longtitles.get(docname) title = title and self.render_partial(title)['title'] or '' self.index_page(docname, doctree, title) def finish(self): self.info(bold('writing additional files...'), nonl=1) # pages from extensions for pagelist in self.app.emit('html-collect-pages'): for pagename, context, template in pagelist: self.handle_page(pagename, context, template) # the global general index if self.get_builder_config('use_index', 'html'): self.write_genindex() # the global domain-specific indices self.write_domain_indices() # the search page if self.name != 'htmlhelp': self.info(' search', nonl=1) self.handle_page('search', {}, 'search.html') # additional pages from conf.py for pagename, template in self.config.html_additional_pages.items(): self.info(' ' + pagename, nonl=1) self.handle_page(pagename, {}, template) if self.config.html_use_opensearch and self.name != 'htmlhelp': self.info(' opensearch', nonl=1) fn = path.join(self.outdir, '_static', 'opensearch.xml') self.handle_page('opensearch', {}, 'opensearch.xml', outfilename=fn) self.info() self.copy_image_files() self.copy_download_files() self.copy_static_files() self.write_buildinfo() # dump the search index self.handle_finish() def write_genindex(self): # the total count of lines for each index letter, used to distribute # the entries into two columns genindex = self.env.create_index(self) indexcounts = [] for _, entries in genindex: indexcounts.append( sum(1 + len(subitems) for _, (_, subitems) in entries)) genindexcontext = dict( genindexentries=genindex, genindexcounts=indexcounts, split_index=self.config.html_split_index, ) self.info(' genindex', nonl=1) if self.config.html_split_index: self.handle_page('genindex', genindexcontext, 'genindex-split.html') self.handle_page('genindex-all', genindexcontext, 'genindex.html') for (key, entries), count in zip(genindex, indexcounts): ctx = { 'key': key, 'entries': entries, 'count': count, 'genindexentries': genindex } self.handle_page('genindex-' + key, ctx, 'genindex-single.html') else: self.handle_page('genindex', genindexcontext, 'genindex.html') def write_domain_indices(self): for indexname, indexcls, content, collapse in self.domain_indices: indexcontext = dict( indextitle=indexcls.localname, content=content, collapse_index=collapse, ) self.info(' ' + indexname, nonl=1) self.handle_page(indexname, indexcontext, 'domainindex.html') def copy_image_files(self): # copy image files if self.images: ensuredir(path.join(self.outdir, '_images')) for src in self.status_iterator(self.images, 'copying images... ', brown, len(self.images)): dest = self.images[src] try: copyfile(path.join(self.srcdir, src), path.join(self.outdir, '_images', dest)) except Exception, err: self.warn('cannot copy image file %r: %s' % (path.join(self.srcdir, src), err))
class StandalonePHPBuilder(Builder): """ Builds standalone PHP docs. """ name = 'php' format = 'php' epilog = __('The PHP pages are in %(outdir)s.') copysource = True allow_parallel = True out_suffix = '.php' link_suffix = '.php' # defaults to matching out_suffix indexer_format = js_index # type: Any indexer_dumps_unicode = True # create links to original images from images [True/False] html_scaled_image_link = True supported_image_types = ['image/svg+xml', 'image/png', 'image/gif', 'image/jpeg'] supported_remote_images = True supported_data_uri_images = True searchindex_filename = 'searchindex.js' add_permalinks = True allow_sharp_as_current_path = True embedded = False # for things like PHP help or Qt help: suppresses sidebar search = True # for things like PHP help and Apple help: suppress search use_index = False download_support = True # enable download role # use html5 translator by default default_html5_translator = False imgpath = None # type: str domain_indices = [] # type: List[Tuple[str, Type[Index], List[Tuple[str, List[IndexEntry]]], bool]] # NOQA # cached publisher object for snippets _publisher = None def __init__(self, app): # type: (Sphinx) -> None super().__init__(app) # CSS files self.css_files = [] # type: List[Dict[str, str]] # JS files self.script_files = JSContainer() # type: List[JavaScript] def init(self): # type: () -> None self.build_info = self.create_build_info() # basename of images directory self.imagedir = '_images' # section numbers for headings in the currently visited document self.secnumbers = {} # type: Dict[str, Tuple[int, ...]] # currently written docname self.current_docname = None # type: str self.init_templates() self.init_highlighter() self.init_css_files() self.init_js_files() html_file_suffix = self.get_builder_config('file_suffix', 'html') if html_file_suffix is not None: self.out_suffix = html_file_suffix html_link_suffix = self.get_builder_config('link_suffix', 'html') if html_link_suffix is not None: self.link_suffix = html_link_suffix else: self.link_suffix = self.out_suffix self.use_index = self.get_builder_config('use_index', 'html') def create_build_info(self): # type: () -> BuildInfo return BuildInfo(self.config, self.tags, ['html']) def _get_translations_js(self): # type: () -> str candidates = [path.join(dir, self.config.language, 'LC_MESSAGES', 'sphinx.js') for dir in self.config.locale_dirs] + \ [path.join(package_dir, 'locale', self.config.language, 'LC_MESSAGES', 'sphinx.js'), path.join(sys.prefix, 'share/sphinx/locale', self.config.language, 'sphinx.js')] for jsfile in candidates: if path.isfile(jsfile): return jsfile return None def get_theme_config(self): # type: () -> Tuple[str, Dict] return self.config.html_theme, self.config.html_theme_options def init_templates(self): # type: () -> None theme_factory = HTMLThemeFactory(self.app) themename, themeoptions = self.get_theme_config() self.theme = theme_factory.create(themename) self.theme_options = themeoptions.copy() self.create_template_bridge() self.templates.init(self, self.theme) def init_highlighter(self): # type: () -> None # determine Pygments style and create the highlighter if self.config.pygments_style is not None: style = self.config.pygments_style elif self.theme: style = self.theme.get_config('theme', 'pygments_style', 'none') else: style = 'sphinx' self.highlighter = PygmentsBridge('html', style) def init_css_files(self): # type: () -> None for filename, attrs in self.app.registry.css_files: self.add_css_file(filename, **attrs) for filename, attrs in self.get_builder_config('css_files', 'html'): self.add_css_file(filename, **attrs) def add_css_file(self, filename, **kwargs): # type: (str, **str) -> None if '://' not in filename: filename = posixpath.join('_static', filename) self.css_files.append(Stylesheet(filename, **kwargs)) # type: ignore def init_js_files(self): # type: () -> None self.add_js_file('jquery.js') self.add_js_file('underscore.js') self.add_js_file('doctools.js') self.add_js_file('language_data.js') for filename, attrs in self.app.registry.js_files: self.add_js_file(filename, **attrs) for filename, attrs in self.get_builder_config('js_files', 'html'): self.add_js_file(filename, **attrs) if self.config.language and self._get_translations_js(): self.add_js_file('translations.js') def add_js_file(self, filename, **kwargs): # type: (str, **str) -> None if filename and '://' not in filename: filename = posixpath.join('_static', filename) self.script_files.append(JavaScript(filename, **kwargs)) @property def default_translator_class(self): # type: ignore # Use HTMLTranslator for PHP builder return HTMLTranslator @property def math_renderer_name(self): # type: () -> str name = self.get_builder_config('math_renderer', 'html') if name is not None: # use given name return name else: # not given: choose a math_renderer from registered ones as possible renderers = list(self.app.registry.html_inline_math_renderers) if len(renderers) == 1: # only default math_renderer (mathjax) is registered return renderers[0] elif len(renderers) == 2: # default and another math_renderer are registered; prior the another renderers.remove('mathjax') return renderers[0] else: # many math_renderers are registered. can't choose automatically! return None def get_outdated_docs(self): # type: () -> Iterator[str] try: with open(path.join(self.outdir, '.buildinfo')) as fp: buildinfo = BuildInfo.load(fp) if self.build_info != buildinfo: yield from self.env.found_docs return except ValueError as exc: logger.warning(__('Failed to read build info file: %r'), exc) except OSError: # ignore errors on reading pass if self.templates: template_mtime = self.templates.newest_template_mtime() else: template_mtime = 0 for docname in self.env.found_docs: if docname not in self.env.all_docs: yield docname continue targetname = self.get_outfilename(docname) try: targetmtime = path.getmtime(targetname) except Exception: targetmtime = 0 try: srcmtime = max(path.getmtime(self.env.doc2path(docname)), template_mtime) if srcmtime > targetmtime: yield docname except OSError: # source doesn't exist anymore pass def get_asset_paths(self): # type: () -> List[str] return self.config.html_extra_path + self.config.html_static_path def render_partial(self, node): # type: (nodes.Node) -> Dict[str, str] """Utility: Render a lone doctree node.""" if node is None: return {'fragment': ''} doc = new_document('<partial node>') doc.append(node) if self._publisher is None: self._publisher = Publisher( source_class = DocTreeInput, destination_class=StringOutput) self._publisher.set_components('standalone', 'restructuredtext', 'pseudoxml') pub = self._publisher pub.reader = DoctreeReader() pub.writer = HTMLWriter(self) pub.process_programmatic_settings( None, {'output_encoding': 'unicode'}, None) pub.set_source(doc, None) pub.set_destination(None, None) pub.publish() return pub.writer.parts def prepare_writing(self, docnames): # type: (Set[str]) -> None # create the search indexer self.indexer = None if self.search: from sphinx.search import IndexBuilder lang = self.config.html_search_language or self.config.language if not lang: lang = 'en' self.indexer = IndexBuilder(self.env, lang, self.config.html_search_options, self.config.html_search_scorer) self.load_indexer(docnames) self.docwriter = HTMLWriter(self) self.docsettings = OptionParser( defaults=self.env.settings, components=(self.docwriter,), read_config_files=True).get_default_values() # type: Any self.docsettings.compact_lists = bool(self.config.html_compact_lists) # determine the additional indices to include self.domain_indices = [] # html_domain_indices can be False/True or a list of index names indices_config = self.config.html_domain_indices if indices_config: for domain_name in sorted(self.env.domains): domain = None # type: Domain domain = self.env.domains[domain_name] for indexcls in domain.indices: indexname = '%s-%s' % (domain.name, indexcls.name) if isinstance(indices_config, list): if indexname not in indices_config: continue content, collapse = indexcls(domain).generate() if content: self.domain_indices.append( (indexname, indexcls, content, collapse)) # format the "last updated on" string, only once is enough since it # typically doesn't include the time of day lufmt = self.config.html_last_updated_fmt if lufmt is not None: self.last_updated = format_date(lufmt or _('%b %d, %Y'), language=self.config.language) else: self.last_updated = None logo = self.config.html_logo and \ path.basename(self.config.html_logo) or '' favicon = self.config.html_favicon and \ path.basename(self.config.html_favicon) or '' if not isinstance(self.config.html_use_opensearch, str): logger.warning(__('html_use_opensearch config value must now be a string')) self.relations = self.env.collect_relations() rellinks = [] # type: List[Tuple[str, str, str, str]] if self.use_index: rellinks.append(('genindex', _('General Index'), 'I', _('index'))) for indexname, indexcls, content, collapse in self.domain_indices: # if it has a short name if indexcls.shortname: rellinks.append((indexname, indexcls.localname, '', indexcls.shortname)) if self.config.html_style is not None: stylename = self.config.html_style elif self.theme: stylename = self.theme.get_config('theme', 'stylesheet') else: stylename = 'default.css' self.globalcontext = { 'embedded': self.embedded, 'project': self.config.project, 'release': return_codes_re.sub('', self.config.release), 'version': self.config.version, 'last_updated': self.last_updated, 'copyright': self.config.copyright, 'master_doc': self.config.master_doc, 'use_opensearch': self.config.html_use_opensearch, 'docstitle': self.config.html_title, 'shorttitle': self.config.html_short_title, 'show_copyright': self.config.html_show_copyright, 'show_sphinx': self.config.html_show_sphinx, 'has_source': self.config.html_copy_source, 'show_source': self.config.html_show_sourcelink, 'sourcelink_suffix': self.config.html_sourcelink_suffix, 'file_suffix': self.out_suffix, 'script_files': self.script_files, 'language': self.config.language, 'css_files': self.css_files, 'sphinx_version': __display_version__, 'style': stylename, 'rellinks': rellinks, 'builder': self.name, 'parents': [], 'logo': logo, 'favicon': favicon } if self.theme: self.globalcontext.update( ('theme_' + key, val) for (key, val) in self.theme.get_options(self.theme_options).items()) self.globalcontext.update(self.config.html_context) def get_doc_context(self, docname, body, metatags): # type: (str, str, str) -> Dict[str, Any] """Collect items for the template context of a page.""" # find out relations prev = next = None parents = [] rellinks = self.globalcontext['rellinks'][:] related = self.relations.get(docname) titles = self.env.titles if related and related[2]: try: next = { 'link': self.get_relative_uri(docname, related[2]), 'title': self.render_partial(titles[related[2]])['title'] } rellinks.append((related[2], next['title'], 'N', _('next'))) except KeyError: next = None if related and related[1]: try: prev = { 'link': self.get_relative_uri(docname, related[1]), 'title': self.render_partial(titles[related[1]])['title'] } rellinks.append((related[1], prev['title'], 'P', _('previous'))) except KeyError: # the relation is (somehow) not in the TOC tree, handle # that gracefully prev = None while related and related[0]: try: parents.append( {'link': self.get_relative_uri(docname, related[0]), 'title': self.render_partial(titles[related[0]])['title']}) except KeyError: pass related = self.relations.get(related[0]) if parents: # remove link to the master file; we have a generic # "back to index" link already parents.pop() parents.reverse() # title rendered as HTML title_node = self.env.longtitles.get(docname) title = title_node and self.render_partial(title_node)['title'] or '' # Suffix for the document source_suffix = path.splitext(self.env.doc2path(docname))[1] # the name for the copied source if self.config.html_copy_source: sourcename = docname + source_suffix if source_suffix != self.config.html_sourcelink_suffix: sourcename += self.config.html_sourcelink_suffix else: sourcename = '' # metadata for the document meta = self.env.metadata.get(docname) # local TOC and global TOC tree self_toc = TocTree(self.env).get_toc_for(docname, self) toc = self.render_partial(self_toc)['fragment'] return { 'parents': parents, 'prev': prev, 'next': next, 'title': title, 'meta': meta, 'body': body, 'metatags': metatags, 'rellinks': rellinks, 'sourcename': sourcename, 'toc': toc, # only display a TOC if there's more than one item to show 'display_toc': (self.env.toc_num_entries[docname] > 1), 'page_source_suffix': source_suffix, } def write_doc(self, docname, doctree): # type: (str, nodes.document) -> None destination = StringOutput(encoding='utf-8') doctree.settings = self.docsettings self.secnumbers = self.env.toc_secnumbers.get(docname, {}) self.fignumbers = self.env.toc_fignumbers.get(docname, {}) self.imgpath = relative_uri(self.get_target_uri(docname), '_images') self.dlpath = relative_uri(self.get_target_uri(docname), '_downloads') self.current_docname = docname self.docwriter.write(doctree, destination) self.docwriter.assemble_parts() body = self.docwriter.parts['fragment'] metatags = self.docwriter.clean_meta ctx = self.get_doc_context(docname, body, metatags) self.handle_page(docname, ctx, event_arg=doctree) def write_doc_serialized(self, docname, doctree): # type: (str, nodes.document) -> None self.imgpath = relative_uri(self.get_target_uri(docname), self.imagedir) self.post_process_images(doctree) title_node = self.env.longtitles.get(docname) title = title_node and self.render_partial(title_node)['title'] or '' self.index_page(docname, doctree, title) def finish(self): # type: () -> None self.finish_tasks.add_task(self.gen_indices) self.finish_tasks.add_task(self.gen_additional_pages) self.finish_tasks.add_task(self.copy_image_files) self.finish_tasks.add_task(self.copy_download_files) self.finish_tasks.add_task(self.copy_static_files) self.finish_tasks.add_task(self.copy_custom_pages) self.finish_tasks.add_task(self.copy_extra_files) self.finish_tasks.add_task(self.write_buildinfo) # dump the search index self.handle_finish() def gen_indices(self): # type: () -> None logger.info(bold(__('generating indices...')), nonl=1) # the global general index if self.use_index: self.write_genindex() # the global domain-specific indices self.write_domain_indices() logger.info('') def gen_additional_pages(self): # type: () -> None # pages from extensions for pagelist in self.app.emit('html-collect-pages'): for pagename, context, template in pagelist: self.handle_page(pagename, context, template) logger.info(bold(__('writing additional pages...')), nonl=1) # additional pages from conf.py for pagename, template in self.config.html_additional_pages.items(): logger.info(' ' + pagename, nonl=1) self.handle_page(pagename, {}, template) # the search page if self.search: logger.info(' search', nonl=1) self.handle_page('search', {}, 'search.html') # the opensearch xml file if self.config.html_use_opensearch and self.search: logger.info(' opensearch', nonl=1) fn = path.join(self.outdir, '_static', 'opensearch.xml') self.handle_page('opensearch', {}, 'opensearch.xml', outfilename=fn) logger.info('') def write_genindex(self): # type: () -> None # the total count of lines for each index letter, used to distribute # the entries into two columns genindex = IndexEntries(self.env).create_index(self) indexcounts = [] for _k, entries in genindex: indexcounts.append(sum(1 + len(subitems) for _, (_, subitems, _) in entries)) genindexcontext = { 'genindexentries': genindex, 'genindexcounts': indexcounts, 'split_index': self.config.html_split_index, } logger.info(' genindex', nonl=1) if self.config.html_split_index: self.handle_page('genindex', genindexcontext, 'genindex-split.html') self.handle_page('genindex-all', genindexcontext, 'genindex.html') for (key, entries), count in zip(genindex, indexcounts): ctx = {'key': key, 'entries': entries, 'count': count, 'genindexentries': genindex} self.handle_page('genindex-' + key, ctx, 'genindex-single.html') else: self.handle_page('genindex', genindexcontext, 'genindex.html') def write_domain_indices(self): # type: () -> None for indexname, indexcls, content, collapse in self.domain_indices: indexcontext = { 'indextitle': indexcls.localname, 'content': content, 'collapse_index': collapse, } logger.info(' ' + indexname, nonl=1) self.handle_page(indexname, indexcontext, 'domainindex.html') def copy_image_files(self): # type: () -> None if self.images: stringify_func = ImageAdapter(self.app.env).get_original_image_uri ensuredir(path.join(self.outdir, self.imagedir)) for src in status_iterator(self.images, __('copying images... '), "brown", len(self.images), self.app.verbosity, stringify_func=stringify_func): dest = self.images[src] try: copyfile(path.join(self.srcdir, src), path.join(self.outdir, self.imagedir, dest)) except Exception as err: logger.warning(__('cannot copy image file %r: %s'), path.join(self.srcdir, src), err) def copy_download_files(self): # type: () -> None def to_relpath(f): # type: (str) -> str return relative_path(self.srcdir, f) # copy downloadable files if self.env.dlfiles: ensuredir(path.join(self.outdir, '_downloads')) for src in status_iterator(self.env.dlfiles, __('copying downloadable files... '), "brown", len(self.env.dlfiles), self.app.verbosity, stringify_func=to_relpath): try: dest = path.join(self.outdir, '_downloads', self.env.dlfiles[src][1]) ensuredir(path.dirname(dest)) copyfile(path.join(self.srcdir, src), dest) except OSError as err: logger.warning(__('cannot copy downloadable file %r: %s'), path.join(self.srcdir, src), err) def copy_custom_pages(self): # then, copy over all user-supplied static files excluded = Matcher(self.config.exclude_patterns + ["**/.*"]) for static_path in self.config.html_static_path: entry = path.join(self.confdir, static_path) if not path.exists(entry): logger.warning(__('html_static_path entry %r does not exist'), entry) continue copy_asset(entry, path.join(self.outdir, '_static'), excluded, context=ctx, renderer=self.templates) def copy_static_files(self): # type: () -> None try: # copy static files logger.info(bold(__('copying static files... ')), nonl=True) ensuredir(path.join(self.outdir, '_static')) # first, create pygments style file with open(path.join(self.outdir, '_static', 'pygments.css'), 'w') as f: f.write(self.highlighter.get_stylesheet()) # then, copy translations JavaScript file if self.config.language is not None: jsfile = self._get_translations_js() if jsfile: copyfile(jsfile, path.join(self.outdir, '_static', 'translations.js')) # copy non-minified stemmer JavaScript file if self.indexer is not None: jsfile = self.indexer.get_js_stemmer_rawcode() if jsfile: copyfile(jsfile, path.join(self.outdir, '_static', '_stemmer.js')) ctx = self.globalcontext.copy() # add context items for search function used in searchtools.js_t if self.indexer is not None: ctx.update(self.indexer.context_for_searchtool()) # then, copy over theme-supplied static files if self.theme: for theme_path in self.theme.get_theme_dirs()[::-1]: entry = path.join(theme_path, 'static') copy_asset(entry, path.join(self.outdir, '_static'), excluded=DOTFILES, context=ctx, renderer=self.templates) # then, copy over all user-supplied static files excluded = Matcher(self.config.exclude_patterns + ["**/.*"]) for static_path in self.config.html_static_path: entry = path.join(self.confdir, static_path) if not path.exists(entry): logger.warning(__('html_static_path entry %r does not exist'), entry) continue copy_asset(entry, path.join(self.outdir, '_static'), excluded, context=ctx, renderer=self.templates) # copy logo and favicon files if not already in static path if self.config.html_logo: logobase = path.basename(self.config.html_logo) logotarget = path.join(self.outdir, '_static', logobase) if not path.isfile(path.join(self.confdir, self.config.html_logo)): logger.warning(__('logo file %r does not exist'), self.config.html_logo) elif not path.isfile(logotarget): copyfile(path.join(self.confdir, self.config.html_logo), logotarget) if self.config.html_favicon: iconbase = path.basename(self.config.html_favicon) icontarget = path.join(self.outdir, '_static', iconbase) if not path.isfile(path.join(self.confdir, self.config.html_favicon)): logger.warning(__('favicon file %r does not exist'), self.config.html_favicon) elif not path.isfile(icontarget): copyfile(path.join(self.confdir, self.config.html_favicon), icontarget) logger.info(__('done')) except OSError as err: logger.warning(__('cannot copy static file %r'), err) def copy_extra_files(self): # type: () -> None try: # copy html_extra_path files logger.info(bold(__('copying extra files... ')), nonl=True) excluded = Matcher(self.config.exclude_patterns) for extra_path in self.config.html_extra_path: entry = path.join(self.confdir, extra_path) if not path.exists(entry): logger.warning(__('html_extra_path entry %r does not exist'), entry) continue copy_asset(entry, self.outdir, excluded) logger.info(__('done')) except OSError as err: logger.warning(__('cannot copy extra file %r'), err) def write_buildinfo(self): # type: () -> None try: with open(path.join(self.outdir, '.buildinfo'), 'w') as fp: self.build_info.dump(fp) except OSError as exc: logger.warning(__('Failed to write build info file: %r'), exc) def cleanup(self): # type: () -> None # clean up theme stuff if self.theme: self.theme.cleanup() def post_process_images(self, doctree): # type: (nodes.Node) -> None """Pick the best candidate for an image and link down-scaled images to their high res version. """ Builder.post_process_images(self, doctree) if self.config.html_scaled_image_link and self.html_scaled_image_link: for node in doctree.traverse(nodes.image): scale_keys = ('scale', 'width', 'height') if not any((key in node) for key in scale_keys) or \ isinstance(node.parent, nodes.reference): # docutils does unfortunately not preserve the # ``target`` attribute on images, so we need to check # the parent node here. continue uri = node['uri'] reference = nodes.reference('', '', internal=True) if uri in self.images: reference['refuri'] = posixpath.join(self.imgpath, self.images[uri]) else: reference['refuri'] = uri node.replace_self(reference) reference.append(node) def load_indexer(self, docnames): # type: (Iterable[str]) -> None keep = set(self.env.all_docs) - set(docnames) try: searchindexfn = path.join(self.outdir, self.searchindex_filename) if self.indexer_dumps_unicode: with open(searchindexfn, encoding='utf-8') as ft: self.indexer.load(ft, self.indexer_format) else: with open(searchindexfn, 'rb') as fb: self.indexer.load(fb, self.indexer_format) except (OSError, ValueError): if keep: logger.warning(__('search index couldn\'t be loaded, but not all ' 'documents will be built: the index will be ' 'incomplete.')) # delete all entries for files that will be rebuilt self.indexer.prune(keep) def index_page(self, pagename, doctree, title): # type: (str, nodes.document, str) -> None # only index pages with title if self.indexer is not None and title: filename = self.env.doc2path(pagename, base=None) try: self.indexer.feed(pagename, filename, title, doctree) except TypeError: # fallback for old search-adapters self.indexer.feed(pagename, title, doctree) # type: ignore indexer_name = self.indexer.__class__.__name__ warnings.warn( 'The %s.feed() method signature is deprecated. Update to ' '%s.feed(docname, filename, title, doctree).' % ( indexer_name, indexer_name), RemovedInSphinx40Warning) def _get_local_toctree(self, docname, collapse=True, **kwds): # type: (str, bool, Any) -> str if 'includehidden' not in kwds: kwds['includehidden'] = False return self.render_partial(TocTree(self.env).get_toctree_for( docname, self, collapse, **kwds))['fragment'] def get_outfilename(self, pagename): # type: (str) -> str return path.join(self.outdir, os_path(pagename) + self.out_suffix) def add_sidebars(self, pagename, ctx): # type: (str, Dict) -> None def has_wildcard(pattern): # type: (str) -> bool return any(char in pattern for char in '*?[') sidebars = None matched = None customsidebar = None # default sidebars settings for selected theme if self.theme.name == 'alabaster': # provide default settings for alabaster (for compatibility) # Note: this will be removed before Sphinx-2.0 try: # get default sidebars settings from alabaster (if defined) theme_default_sidebars = self.theme.config.get('theme', 'sidebars') if theme_default_sidebars: sidebars = [name.strip() for name in theme_default_sidebars.split(',')] except Exception: # fallback to better default settings sidebars = ['about.html', 'navigation.html', 'relations.html', 'searchbox.html', 'donate.html'] else: theme_default_sidebars = self.theme.get_config('theme', 'sidebars', None) if theme_default_sidebars: sidebars = [name.strip() for name in theme_default_sidebars.split(',')] # user sidebar settings html_sidebars = self.get_builder_config('sidebars', 'html') for pattern, patsidebars in html_sidebars.items(): if patmatch(pagename, pattern): if matched: if has_wildcard(pattern): # warn if both patterns contain wildcards if has_wildcard(matched): logger.warning(__('page %s matches two patterns in ' 'html_sidebars: %r and %r'), pagename, matched, pattern) # else the already matched pattern is more specific # than the present one, because it contains no wildcard continue matched = pattern sidebars = patsidebars if sidebars is None: # keep defaults pass ctx['sidebars'] = sidebars ctx['customsidebar'] = customsidebar # --------- these are overwritten by the serialization builder def get_target_uri(self, docname, typ=None): # type: (str, str) -> str return docname + self.link_suffix def handle_page(self, pagename, addctx, templatename='page.html', outfilename=None, event_arg=None): # type: (str, Dict, str, str, Any) -> None ctx = self.globalcontext.copy() # current_page_name is backwards compatibility ctx['pagename'] = ctx['current_page_name'] = pagename ctx['encoding'] = self.config.html_output_encoding default_baseuri = self.get_target_uri(pagename) # in the singlehtml builder, default_baseuri still contains an #anchor # part, which relative_uri doesn't really like... default_baseuri = default_baseuri.rsplit('#', 1)[0] if self.config.html_baseurl: ctx['pageurl'] = posixpath.join(self.config.html_baseurl, pagename + self.out_suffix) else: ctx['pageurl'] = None def pathto(otheruri, resource=False, baseuri=default_baseuri): # type: (str, bool, str) -> str if resource and '://' in otheruri: # allow non-local resources given by scheme return otheruri elif not resource: otheruri = self.get_target_uri(otheruri) uri = relative_uri(baseuri, otheruri) or '#' if uri == '#' and not self.allow_sharp_as_current_path: uri = baseuri return uri ctx['pathto'] = pathto def css_tag(css): # type: (Stylesheet) -> str attrs = [] for key in sorted(css.attributes): value = css.attributes[key] if value is not None: attrs.append('%s="%s"' % (key, html.escape(value, True))) attrs.append('href="%s"' % pathto(css.filename, resource=True)) return '<link %s />' % ' '.join(attrs) ctx['css_tag'] = css_tag def hasdoc(name): # type: (str) -> bool if name in self.env.all_docs: return True elif name == 'search' and self.search: return True elif name == 'genindex' and self.get_builder_config('use_index', 'html'): return True return False ctx['hasdoc'] = hasdoc def warn(*args, **kwargs): # type: (Any, Any) -> str """Simple warn() wrapper for themes.""" warnings.warn('The template function warn() was deprecated. ' 'Use warning() instead.', RemovedInSphinx30Warning, stacklevel=2) logger.warning(*args, **kwargs) return '' # return empty string ctx['warn'] = warn ctx['toctree'] = lambda **kw: self._get_local_toctree(pagename, **kw) self.add_sidebars(pagename, ctx) ctx.update(addctx) self.update_page_context(pagename, templatename, ctx, event_arg) newtmpl = self.app.emit_firstresult('html-page-context', pagename, templatename, ctx, event_arg) if newtmpl: templatename = newtmpl try: output = self.templates.render(templatename, ctx) except UnicodeError: logger.warning(__("a Unicode error occurred when rendering the page %s. " "Please make sure all config values that contain " "non-ASCII content are Unicode strings."), pagename) return except Exception as exc: raise ThemeError(__("An error happened in rendering the page %s.\nReason: %r") % (pagename, exc)) if not outfilename: outfilename = self.get_outfilename(pagename) # outfilename's path is in general different from self.outdir ensuredir(path.dirname(outfilename)) try: with open(outfilename, 'w', encoding=ctx['encoding'], errors='xmlcharrefreplace') as f: f.write(output) except OSError as err: logger.warning(__("error writing file %s: %s"), outfilename, err) if self.copysource and ctx.get('sourcename'): # copy the source file for the "show source" link source_name = path.join(self.outdir, '_sources', os_path(ctx['sourcename'])) ensuredir(path.dirname(source_name)) copyfile(self.env.doc2path(pagename), source_name) def update_page_context(self, pagename, templatename, ctx, event_arg): # type: (str, str, Dict, Any) -> None pass def handle_finish(self): # type: () -> None if self.indexer: self.finish_tasks.add_task(self.dump_search_index) self.finish_tasks.add_task(self.dump_inventory) def dump_inventory(self): # type: () -> None logger.info(bold(__('dumping object inventory... ')), nonl=True) InventoryFile.dump(path.join(self.outdir, INVENTORY_FILENAME), self.env, self) logger.info(__('done')) def dump_search_index(self): # type: () -> None logger.info( bold(__('dumping search index in %s ... ') % self.indexer.label()), nonl=True) self.indexer.prune(self.env.all_docs) searchindexfn = path.join(self.outdir, self.searchindex_filename) # first write to a temporary file, so that if dumping fails, # the existing index won't be overwritten if self.indexer_dumps_unicode: with open(searchindexfn + '.tmp', 'w', encoding='utf-8') as ft: self.indexer.dump(ft, self.indexer_format) else: with open(searchindexfn + '.tmp', 'wb') as fb: self.indexer.dump(fb, self.indexer_format) movefile(searchindexfn + '.tmp', searchindexfn) logger.info(__('done'))
enable_exit_status=1 argv=None usage=default_usage description=default_description # Argument values given to publish_cmdline() in rst2latex.py:: description = ('Generates PDF documents from standalone reStructuredText ' 'sources using the "latex" Writer and the "rubber" ' 'building system for LaTeX documents. ' + default_description) writer_name = 'latex' # Set up the publisher:: pub = Publisher(reader, parser, writer, settings=settings) pub.set_components(reader_name, parser_name, writer_name) # Parse the command line args # (Publisher.publish does this in a try statement):: pub.process_command_line(argv, usage, description, settings_spec, config_section, **(settings_overrides or {})) # pprint(pub.settings.__dict__) # Get source and destination path:: source = pub.settings._source destination = pub.settings._destination # print source, destination # Generate names for the temporary files and set ``destination`` to temporary
class StandaloneHTMLBuilder(Builder): """ Builds standalone HTML docs. """ name = 'html' format = 'html' copysource = True allow_parallel = True out_suffix = '.html' link_suffix = '.html' # defaults to matching out_suffix indexer_format = js_index indexer_dumps_unicode = True supported_image_types = ['image/svg+xml', 'image/png', 'image/gif', 'image/jpeg'] searchindex_filename = 'searchindex.js' add_permalinks = True embedded = False # for things like HTML help or Qt help: suppresses sidebar search = True # for things like HTML help and Apple help: suppress search # This is a class attribute because it is mutated by Sphinx.add_javascript. script_files = ['_static/jquery.js', '_static/underscore.js', '_static/doctools.js'] # Dito for this one. css_files = [] default_sidebars = ['localtoc.html', 'relations.html', 'sourcelink.html', 'searchbox.html'] # cached publisher object for snippets _publisher = None def init(self): # a hash of all config values that, if changed, cause a full rebuild self.config_hash = '' self.tags_hash = '' # basename of images directory self.imagedir = '_images' # section numbers for headings in the currently visited document self.secnumbers = {} # currently written docname self.current_docname = None self.init_templates() self.init_highlighter() self.init_translator_class() if self.config.html_file_suffix is not None: self.out_suffix = self.config.html_file_suffix if self.config.html_link_suffix is not None: self.link_suffix = self.config.html_link_suffix else: self.link_suffix = self.out_suffix if self.config.language is not None: if self._get_translations_js(): self.script_files.append('_static/translations.js') def _get_translations_js(self): candidates = [path.join(package_dir, 'locale', self.config.language, 'LC_MESSAGES', 'sphinx.js'), path.join(sys.prefix, 'share/sphinx/locale', self.config.language, 'sphinx.js')] + \ [path.join(dir, self.config.language, 'LC_MESSAGES', 'sphinx.js') for dir in self.config.locale_dirs] for jsfile in candidates: if path.isfile(jsfile): return jsfile return None def get_theme_config(self): return self.config.html_theme, self.config.html_theme_options def init_templates(self): Theme.init_themes(self.confdir, self.config.html_theme_path, warn=self.warn) themename, themeoptions = self.get_theme_config() self.theme = Theme(themename, warn=self.warn) self.theme_options = themeoptions.copy() self.create_template_bridge() self.templates.init(self, self.theme) def init_highlighter(self): # determine Pygments style and create the highlighter if self.config.pygments_style is not None: style = self.config.pygments_style elif self.theme: style = self.theme.get_confstr('theme', 'pygments_style', 'none') else: style = 'sphinx' self.highlighter = PygmentsBridge('html', style, self.config.trim_doctest_flags) def init_translator_class(self): if self.translator_class is not None: pass elif self.config.html_translator_class: self.translator_class = self.app.import_object( self.config.html_translator_class, 'html_translator_class setting') elif self.config.html_use_smartypants: self.translator_class = SmartyPantsHTMLTranslator else: self.translator_class = HTMLTranslator def get_outdated_docs(self): cfgdict = dict((name, self.config[name]) for (name, desc) in iteritems(self.config.values) if desc[1] == 'html') self.config_hash = get_stable_hash(cfgdict) self.tags_hash = get_stable_hash(sorted(self.tags)) old_config_hash = old_tags_hash = '' try: fp = open(path.join(self.outdir, '.buildinfo')) try: version = fp.readline() if version.rstrip() != '# Sphinx build info version 1': raise ValueError fp.readline() # skip commentary cfg, old_config_hash = fp.readline().strip().split(': ') if cfg != 'config': raise ValueError tag, old_tags_hash = fp.readline().strip().split(': ') if tag != 'tags': raise ValueError finally: fp.close() except ValueError: self.warn('unsupported build info format in %r, building all' % path.join(self.outdir, '.buildinfo')) except Exception: pass if old_config_hash != self.config_hash or \ old_tags_hash != self.tags_hash: for docname in self.env.found_docs: yield docname return if self.templates: template_mtime = self.templates.newest_template_mtime() else: template_mtime = 0 for docname in self.env.found_docs: if docname not in self.env.all_docs: yield docname continue targetname = self.get_outfilename(docname) try: targetmtime = path.getmtime(targetname) except Exception: targetmtime = 0 try: srcmtime = max(path.getmtime(self.env.doc2path(docname)), template_mtime) if srcmtime > targetmtime: yield docname except EnvironmentError: # source doesn't exist anymore pass def render_partial(self, node): """Utility: Render a lone doctree node.""" if node is None: return {'fragment': ''} doc = new_document(b'<partial node>') doc.append(node) if self._publisher is None: self._publisher = Publisher( source_class = DocTreeInput, destination_class=StringOutput) self._publisher.set_components('standalone', 'restructuredtext', 'pseudoxml') pub = self._publisher pub.reader = DoctreeReader() pub.writer = HTMLWriter(self) pub.process_programmatic_settings( None, {'output_encoding': 'unicode'}, None) pub.set_source(doc, None) pub.set_destination(None, None) pub.publish() return pub.writer.parts def prepare_writing(self, docnames): # create the search indexer self.indexer = None if self.search: from sphinx.search import IndexBuilder, languages lang = self.config.html_search_language or self.config.language if not lang or lang not in languages: lang = 'en' self.indexer = IndexBuilder(self.env, lang, self.config.html_search_options, self.config.html_search_scorer) self.load_indexer(docnames) self.docwriter = HTMLWriter(self) self.docsettings = OptionParser( defaults=self.env.settings, components=(self.docwriter,), read_config_files=True).get_default_values() self.docsettings.compact_lists = bool(self.config.html_compact_lists) # determine the additional indices to include self.domain_indices = [] # html_domain_indices can be False/True or a list of index names indices_config = self.config.html_domain_indices if indices_config: for domain_name in sorted(self.env.domains): domain = self.env.domains[domain_name] for indexcls in domain.indices: indexname = '%s-%s' % (domain.name, indexcls.name) if isinstance(indices_config, list): if indexname not in indices_config: continue # deprecated config value if indexname == 'py-modindex' and \ not self.config.html_use_modindex: continue content, collapse = indexcls(domain).generate() if content: self.domain_indices.append( (indexname, indexcls, content, collapse)) # format the "last updated on" string, only once is enough since it # typically doesn't include the time of day lufmt = self.config.html_last_updated_fmt if lufmt is not None: self.last_updated = format_date(lufmt or _('MMM dd, YYYY'), language=self.config.language) else: self.last_updated = None logo = self.config.html_logo and \ path.basename(self.config.html_logo) or '' favicon = self.config.html_favicon and \ path.basename(self.config.html_favicon) or '' if favicon and os.path.splitext(favicon)[1] != '.ico': self.warn('html_favicon is not an .ico file') if not isinstance(self.config.html_use_opensearch, string_types): self.warn('html_use_opensearch config value must now be a string') self.relations = self.env.collect_relations() rellinks = [] if self.get_builder_config('use_index', 'html'): rellinks.append(('genindex', _('General Index'), 'I', _('index'))) for indexname, indexcls, content, collapse in self.domain_indices: # if it has a short name if indexcls.shortname: rellinks.append((indexname, indexcls.localname, '', indexcls.shortname)) if self.config.html_style is not None: stylename = self.config.html_style elif self.theme: stylename = self.theme.get_confstr('theme', 'stylesheet') else: stylename = 'default.css' self.globalcontext = dict( embedded = self.embedded, project = self.config.project, release = self.config.release, version = self.config.version, last_updated = self.last_updated, copyright = self.config.copyright, master_doc = self.config.master_doc, use_opensearch = self.config.html_use_opensearch, docstitle = self.config.html_title, shorttitle = self.config.html_short_title, show_copyright = self.config.html_show_copyright, show_sphinx = self.config.html_show_sphinx, has_source = self.config.html_copy_source, show_source = self.config.html_show_sourcelink, file_suffix = self.out_suffix, script_files = self.script_files, language = self.config.language, css_files = self.css_files, sphinx_version = __display_version__, style = stylename, rellinks = rellinks, builder = self.name, parents = [], logo = logo, favicon = favicon, ) if self.theme: self.globalcontext.update( ('theme_' + key, val) for (key, val) in iteritems(self.theme.get_options(self.theme_options))) self.globalcontext.update(self.config.html_context) def get_doc_context(self, docname, body, metatags): """Collect items for the template context of a page.""" # find out relations prev = next = None parents = [] rellinks = self.globalcontext['rellinks'][:] related = self.relations.get(docname) titles = self.env.titles if related and related[2]: try: next = { 'link': self.get_relative_uri(docname, related[2]), 'title': self.render_partial(titles[related[2]])['title'] } rellinks.append((related[2], next['title'], 'N', _('next'))) except KeyError: next = None if related and related[1]: try: prev = { 'link': self.get_relative_uri(docname, related[1]), 'title': self.render_partial(titles[related[1]])['title'] } rellinks.append((related[1], prev['title'], 'P', _('previous'))) except KeyError: # the relation is (somehow) not in the TOC tree, handle # that gracefully prev = None while related and related[0]: try: parents.append( {'link': self.get_relative_uri(docname, related[0]), 'title': self.render_partial(titles[related[0]])['title']}) except KeyError: pass related = self.relations.get(related[0]) if parents: # remove link to the master file; we have a generic # "back to index" link already parents.pop() parents.reverse() # title rendered as HTML title = self.env.longtitles.get(docname) title = title and self.render_partial(title)['title'] or '' # the name for the copied source sourcename = self.config.html_copy_source and docname + '.txt' or '' # metadata for the document meta = self.env.metadata.get(docname) # Suffix for the document source_suffix = '.' + self.env.doc2path(docname).split('.')[-1] # local TOC and global TOC tree self_toc = self.env.get_toc_for(docname, self) toc = self.render_partial(self_toc)['fragment'] return dict( parents = parents, prev = prev, next = next, title = title, meta = meta, body = body, metatags = metatags, rellinks = rellinks, sourcename = sourcename, toc = toc, # only display a TOC if there's more than one item to show display_toc = (self.env.toc_num_entries[docname] > 1), page_source_suffix = source_suffix, ) def write_doc(self, docname, doctree): destination = StringOutput(encoding='utf-8') doctree.settings = self.docsettings self.secnumbers = self.env.toc_secnumbers.get(docname, {}) self.fignumbers = self.env.toc_fignumbers.get(docname, {}) self.imgpath = relative_uri(self.get_target_uri(docname), '_images') self.dlpath = relative_uri(self.get_target_uri(docname), '_downloads') self.current_docname = docname self.docwriter.write(doctree, destination) self.docwriter.assemble_parts() body = self.docwriter.parts['fragment'] metatags = self.docwriter.clean_meta ctx = self.get_doc_context(docname, body, metatags) self.handle_page(docname, ctx, event_arg=doctree) def write_doc_serialized(self, docname, doctree): self.imgpath = relative_uri(self.get_target_uri(docname), self.imagedir) self.post_process_images(doctree) title = self.env.longtitles.get(docname) title = title and self.render_partial(title)['title'] or '' self.index_page(docname, doctree, title) def finish(self): self.finish_tasks.add_task(self.gen_indices) self.finish_tasks.add_task(self.gen_additional_pages) self.finish_tasks.add_task(self.copy_image_files) self.finish_tasks.add_task(self.copy_download_files) self.finish_tasks.add_task(self.copy_static_files) self.finish_tasks.add_task(self.copy_extra_files) self.finish_tasks.add_task(self.write_buildinfo) # dump the search index self.handle_finish() def gen_indices(self): self.info(bold('generating indices...'), nonl=1) # the global general index if self.get_builder_config('use_index', 'html'): self.write_genindex() # the global domain-specific indices self.write_domain_indices() self.info() def gen_additional_pages(self): # pages from extensions for pagelist in self.app.emit('html-collect-pages'): for pagename, context, template in pagelist: self.handle_page(pagename, context, template) self.info(bold('writing additional pages...'), nonl=1) # additional pages from conf.py for pagename, template in self.config.html_additional_pages.items(): self.info(' '+pagename, nonl=1) self.handle_page(pagename, {}, template) # the search page if self.search: self.info(' search', nonl=1) self.handle_page('search', {}, 'search.html') # the opensearch xml file if self.config.html_use_opensearch and self.search: self.info(' opensearch', nonl=1) fn = path.join(self.outdir, '_static', 'opensearch.xml') self.handle_page('opensearch', {}, 'opensearch.xml', outfilename=fn) self.info() def write_genindex(self): # the total count of lines for each index letter, used to distribute # the entries into two columns genindex = self.env.create_index(self) indexcounts = [] for _k, entries in genindex: indexcounts.append(sum(1 + len(subitems) for _, (_, subitems, _) in entries)) genindexcontext = dict( genindexentries = genindex, genindexcounts = indexcounts, split_index = self.config.html_split_index, ) self.info(' genindex', nonl=1) if self.config.html_split_index: self.handle_page('genindex', genindexcontext, 'genindex-split.html') self.handle_page('genindex-all', genindexcontext, 'genindex.html') for (key, entries), count in zip(genindex, indexcounts): ctx = {'key': key, 'entries': entries, 'count': count, 'genindexentries': genindex} self.handle_page('genindex-' + key, ctx, 'genindex-single.html') else: self.handle_page('genindex', genindexcontext, 'genindex.html') def write_domain_indices(self): for indexname, indexcls, content, collapse in self.domain_indices: indexcontext = dict( indextitle = indexcls.localname, content = content, collapse_index = collapse, ) self.info(' ' + indexname, nonl=1) self.handle_page(indexname, indexcontext, 'domainindex.html') def copy_image_files(self): # copy image files if self.images: ensuredir(path.join(self.outdir, self.imagedir)) for src in self.app.status_iterator(self.images, 'copying images... ', brown, len(self.images)): dest = self.images[src] try: copyfile(path.join(self.srcdir, src), path.join(self.outdir, self.imagedir, dest)) except Exception as err: self.warn('cannot copy image file %r: %s' % (path.join(self.srcdir, src), err)) def copy_download_files(self): def to_relpath(f): return relative_path(self.srcdir, f) # copy downloadable files if self.env.dlfiles: ensuredir(path.join(self.outdir, '_downloads')) for src in self.app.status_iterator(self.env.dlfiles, 'copying downloadable files... ', brown, len(self.env.dlfiles), stringify_func=to_relpath): dest = self.env.dlfiles[src][1] try: copyfile(path.join(self.srcdir, src), path.join(self.outdir, '_downloads', dest)) except Exception as err: self.warn('cannot copy downloadable file %r: %s' % (path.join(self.srcdir, src), err)) def copy_static_files(self): # copy static files self.info(bold('copying static files... '), nonl=True) ensuredir(path.join(self.outdir, '_static')) # first, create pygments style file f = open(path.join(self.outdir, '_static', 'pygments.css'), 'w') f.write(self.highlighter.get_stylesheet()) f.close() # then, copy translations JavaScript file if self.config.language is not None: jsfile = self._get_translations_js() if jsfile: copyfile(jsfile, path.join(self.outdir, '_static', 'translations.js')) # copy non-minified stemmer JavaScript file if self.indexer is not None: jsfile = self.indexer.get_js_stemmer_rawcode() if jsfile: copyfile(jsfile, path.join(self.outdir, '_static', '_stemmer.js')) ctx = self.globalcontext.copy() # add context items for search function used in searchtools.js_t if self.indexer is not None: ctx.update(self.indexer.context_for_searchtool()) # then, copy over theme-supplied static files if self.theme: themeentries = [path.join(themepath, 'static') for themepath in self.theme.get_dirchain()[::-1]] for entry in themeentries: copy_static_entry(entry, path.join(self.outdir, '_static'), self, ctx) # then, copy over all user-supplied static files staticentries = [path.join(self.confdir, spath) for spath in self.config.html_static_path] matchers = compile_matchers(self.config.exclude_patterns) for entry in staticentries: if not path.exists(entry): self.warn('html_static_path entry %r does not exist' % entry) continue copy_static_entry(entry, path.join(self.outdir, '_static'), self, ctx, exclude_matchers=matchers) # copy logo and favicon files if not already in static path if self.config.html_logo: logobase = path.basename(self.config.html_logo) logotarget = path.join(self.outdir, '_static', logobase) if not path.isfile(path.join(self.confdir, self.config.html_logo)): self.warn('logo file %r does not exist' % self.config.html_logo) elif not path.isfile(logotarget): copyfile(path.join(self.confdir, self.config.html_logo), logotarget) if self.config.html_favicon: iconbase = path.basename(self.config.html_favicon) icontarget = path.join(self.outdir, '_static', iconbase) if not path.isfile(path.join(self.confdir, self.config.html_favicon)): self.warn('favicon file %r does not exist' % self.config.html_favicon) elif not path.isfile(icontarget): copyfile(path.join(self.confdir, self.config.html_favicon), icontarget) self.info('done') def copy_extra_files(self): # copy html_extra_path files self.info(bold('copying extra files... '), nonl=True) extraentries = [path.join(self.confdir, epath) for epath in self.config.html_extra_path] matchers = compile_matchers(self.config.exclude_patterns) for entry in extraentries: if not path.exists(entry): self.warn('html_extra_path entry %r does not exist' % entry) continue copy_extra_entry(entry, self.outdir, matchers) self.info('done') def write_buildinfo(self): # write build info file fp = open(path.join(self.outdir, '.buildinfo'), 'w') try: fp.write('# Sphinx build info version 1\n' '# This file hashes the configuration used when building' ' these files. When it is not found, a full rebuild will' ' be done.\nconfig: %s\ntags: %s\n' % (self.config_hash, self.tags_hash)) finally: fp.close() def cleanup(self): # clean up theme stuff if self.theme: self.theme.cleanup() def post_process_images(self, doctree): """Pick the best candidate for an image and link down-scaled images to their high res version. """ Builder.post_process_images(self, doctree) if self.config.html_scaled_image_link: for node in doctree.traverse(nodes.image): scale_keys = ('scale', 'width', 'height') if not any((key in node) for key in scale_keys) or \ isinstance(node.parent, nodes.reference): # docutils does unfortunately not preserve the # ``target`` attribute on images, so we need to check # the parent node here. continue uri = node['uri'] reference = nodes.reference('', '', internal=True) if uri in self.images: reference['refuri'] = posixpath.join(self.imgpath, self.images[uri]) else: reference['refuri'] = uri node.replace_self(reference) reference.append(node) def load_indexer(self, docnames): keep = set(self.env.all_docs) - set(docnames) try: searchindexfn = path.join(self.outdir, self.searchindex_filename) if self.indexer_dumps_unicode: f = codecs.open(searchindexfn, 'r', encoding='utf-8') else: f = open(searchindexfn, 'rb') try: self.indexer.load(f, self.indexer_format) finally: f.close() except (IOError, OSError, ValueError): if keep: self.warn('search index couldn\'t be loaded, but not all ' 'documents will be built: the index will be ' 'incomplete.') # delete all entries for files that will be rebuilt self.indexer.prune(keep) def index_page(self, pagename, doctree, title): # only index pages with title if self.indexer is not None and title: self.indexer.feed(pagename, title, doctree) def _get_local_toctree(self, docname, collapse=True, **kwds): if 'includehidden' not in kwds: kwds['includehidden'] = False return self.render_partial(self.env.get_toctree_for( docname, self, collapse, **kwds))['fragment'] def get_outfilename(self, pagename): return path.join(self.outdir, os_path(pagename) + self.out_suffix) def add_sidebars(self, pagename, ctx): def has_wildcard(pattern): return any(char in pattern for char in '*?[') sidebars = None matched = None customsidebar = None for pattern, patsidebars in iteritems(self.config.html_sidebars): if patmatch(pagename, pattern): if matched: if has_wildcard(pattern): # warn if both patterns contain wildcards if has_wildcard(matched): self.warn('page %s matches two patterns in ' 'html_sidebars: %r and %r' % (pagename, matched, pattern)) # else the already matched pattern is more specific # than the present one, because it contains no wildcard continue matched = pattern sidebars = patsidebars if sidebars is None: # keep defaults pass elif isinstance(sidebars, string_types): # 0.x compatible mode: insert custom sidebar before searchbox customsidebar = sidebars sidebars = None ctx['sidebars'] = sidebars ctx['customsidebar'] = customsidebar # --------- these are overwritten by the serialization builder def get_target_uri(self, docname, typ=None): return docname + self.link_suffix def handle_page(self, pagename, addctx, templatename='page.html', outfilename=None, event_arg=None): ctx = self.globalcontext.copy() # current_page_name is backwards compatibility ctx['pagename'] = ctx['current_page_name'] = pagename default_baseuri = self.get_target_uri(pagename) # in the singlehtml builder, default_baseuri still contains an #anchor # part, which relative_uri doesn't really like... default_baseuri = default_baseuri.rsplit('#', 1)[0] def pathto(otheruri, resource=False, baseuri=default_baseuri): if resource and '://' in otheruri: # allow non-local resources given by scheme return otheruri elif not resource: otheruri = self.get_target_uri(otheruri) uri = relative_uri(baseuri, otheruri) or '#' return uri ctx['pathto'] = pathto ctx['hasdoc'] = lambda name: name in self.env.all_docs if self.name != 'htmlhelp': ctx['encoding'] = encoding = self.config.html_output_encoding else: ctx['encoding'] = encoding = self.encoding ctx['toctree'] = lambda **kw: self._get_local_toctree(pagename, **kw) self.add_sidebars(pagename, ctx) ctx.update(addctx) newtmpl = self.app.emit_firstresult('html-page-context', pagename, templatename, ctx, event_arg) if newtmpl: templatename = newtmpl try: output = self.templates.render(templatename, ctx) except UnicodeError: self.warn("a Unicode error occurred when rendering the page %s. " "Please make sure all config values that contain " "non-ASCII content are Unicode strings." % pagename) return if not outfilename: outfilename = self.get_outfilename(pagename) # outfilename's path is in general different from self.outdir ensuredir(path.dirname(outfilename)) try: f = codecs.open(outfilename, 'w', encoding, 'xmlcharrefreplace') try: f.write(output) finally: f.close() except (IOError, OSError) as err: self.warn("error writing file %s: %s" % (outfilename, err)) if self.copysource and ctx.get('sourcename'): # copy the source file for the "show source" link source_name = path.join(self.outdir, '_sources', os_path(ctx['sourcename'])) ensuredir(path.dirname(source_name)) copyfile(self.env.doc2path(pagename), source_name) def handle_finish(self): if self.indexer: self.finish_tasks.add_task(self.dump_search_index) self.finish_tasks.add_task(self.dump_inventory) def dump_inventory(self): self.info(bold('dumping object inventory... '), nonl=True) f = open(path.join(self.outdir, INVENTORY_FILENAME), 'wb') try: f.write((u'# Sphinx inventory version 2\n' u'# Project: %s\n' u'# Version: %s\n' u'# The remainder of this file is compressed using zlib.\n' % (self.config.project, self.config.version)).encode('utf-8')) compressor = zlib.compressobj(9) for domainname, domain in sorted(self.env.domains.items()): for name, dispname, type, docname, anchor, prio in \ sorted(domain.get_objects()): if anchor.endswith(name): # this can shorten the inventory by as much as 25% anchor = anchor[:-len(name)] + '$' uri = self.get_target_uri(docname) if anchor: uri += '#' + anchor if dispname == name: dispname = u'-' f.write(compressor.compress( (u'%s %s:%s %s %s %s\n' % (name, domainname, type, prio, uri, dispname)).encode('utf-8'))) f.write(compressor.flush()) finally: f.close() self.info('done') def dump_search_index(self): self.info( bold('dumping search index in %s ... ' % self.indexer.label()), nonl=True) self.indexer.prune(self.env.all_docs) searchindexfn = path.join(self.outdir, self.searchindex_filename) # first write to a temporary file, so that if dumping fails, # the existing index won't be overwritten if self.indexer_dumps_unicode: f = codecs.open(searchindexfn + '.tmp', 'w', encoding='utf-8') else: f = open(searchindexfn + '.tmp', 'wb') try: self.indexer.dump(f, self.indexer_format) finally: f.close() movefile(searchindexfn + '.tmp', searchindexfn) self.info('done')
def read_doc(self, docname, app=None): # type: (unicode, Sphinx) -> None """Parse a file and add/update inventory entries for the doctree.""" self.temp_data['docname'] = docname # defaults to the global default, but can be re-set in a document self.temp_data['default_domain'] = \ self.domains.get(self.config.primary_domain) self.settings['input_encoding'] = self.config.source_encoding self.settings['trim_footnote_reference_space'] = \ self.config.trim_footnote_reference_space self.settings['gettext_compact'] = self.config.gettext_compact docutilsconf = path.join(self.srcdir, 'docutils.conf') # read docutils.conf from source dir, not from current dir OptionParser.standard_config_files[1] = docutilsconf if path.isfile(docutilsconf): self.note_dependency(docutilsconf) with sphinx_domains(self): if self.config.default_role: role_fn, messages = roles.role(self.config.default_role, english, 0, dummy_reporter) if role_fn: roles._roles[''] = role_fn else: logger.warning('default role %s not found', self.config.default_role, location=docname) codecs.register_error('sphinx', self.warn_and_replace) # type: ignore # publish manually reader = SphinxStandaloneReader(self.app, parsers=self.config.source_parsers) pub = Publisher(reader=reader, writer=SphinxDummyWriter(), destination_class=NullOutput) pub.set_components(None, 'restructuredtext', None) pub.process_programmatic_settings(None, self.settings, None) src_path = self.doc2path(docname) source = SphinxFileInput(app, self, source=None, source_path=src_path, encoding=self.config.source_encoding) pub.source = source pub.settings._source = src_path pub.set_destination(None, None) pub.publish() doctree = pub.document # post-processing for domain in itervalues(self.domains): domain.process_doc(self, docname, doctree) # allow extension-specific post-processing if app: app.emit('doctree-read', doctree) # store time of reading, for outdated files detection # (Some filesystems have coarse timestamp resolution; # therefore time.time() can be older than filesystem's timestamp. # For example, FAT32 has 2sec timestamp resolution.) self.all_docs[docname] = max( time.time(), path.getmtime(self.doc2path(docname))) if self.versioning_condition: old_doctree = None if self.versioning_compare: # get old doctree try: with open(self.doc2path(docname, self.doctreedir, '.doctree'), 'rb') as f: old_doctree = pickle.load(f) except EnvironmentError: pass # add uids for versioning if not self.versioning_compare or old_doctree is None: list(add_uids(doctree, self.versioning_condition)) else: list(merge_doctrees( old_doctree, doctree, self.versioning_condition)) # make it picklable doctree.reporter = None doctree.transformer = None doctree.settings.warning_stream = None doctree.settings.env = None doctree.settings.record_dependencies = None # cleanup self.temp_data.clear() self.ref_context.clear() roles._roles.pop('', None) # if a document has set a local default role # save the parsed doctree doctree_filename = self.doc2path(docname, self.doctreedir, '.doctree') ensuredir(path.dirname(doctree_filename)) with open(doctree_filename, 'wb') as f: pickle.dump(doctree, f, pickle.HIGHEST_PROTOCOL)
class StandaloneHTMLBuilder(Builder): """ Builds standalone HTML docs. """ name = 'html' format = 'html' copysource = True allow_parallel = True out_suffix = '.html' link_suffix = '.html' # defaults to matching out_suffix indexer_format = js_index indexer_dumps_unicode = True supported_image_types = [ 'image/svg+xml', 'image/png', 'image/gif', 'image/jpeg' ] searchindex_filename = 'searchindex.js' add_permalinks = True embedded = False # for things like HTML help or Qt help: suppresses sidebar # This is a class attribute because it is mutated by Sphinx.add_javascript. script_files = [ '_static/jquery.js', '_static/underscore.js', '_static/doctools.js' ] # Dito for this one. css_files = [] default_sidebars = [ 'localtoc.html', 'relations.html', 'sourcelink.html', 'searchbox.html' ] # cached publisher object for snippets _publisher = None def init(self): # a hash of all config values that, if changed, cause a full rebuild self.config_hash = '' self.tags_hash = '' # section numbers for headings in the currently visited document self.secnumbers = {} # currently written docname self.current_docname = None self.init_templates() self.init_highlighter() self.init_translator_class() if self.config.html_file_suffix is not None: self.out_suffix = self.config.html_file_suffix if self.config.html_link_suffix is not None: self.link_suffix = self.config.html_link_suffix else: self.link_suffix = self.out_suffix if self.config.language is not None: if self._get_translations_js(): self.script_files.append('_static/translations.js') def _get_translations_js(self): candidates = [path.join(package_dir, 'locale', self.config.language, 'LC_MESSAGES', 'sphinx.js'), path.join(sys.prefix, 'share/sphinx/locale', self.config.language, 'sphinx.js')] + \ [path.join(dir, self.config.language, 'LC_MESSAGES', 'sphinx.js') for dir in self.config.locale_dirs] for jsfile in candidates: if path.isfile(jsfile): return jsfile return None def get_theme_config(self): return self.config.html_theme, self.config.html_theme_options def init_templates(self): Theme.init_themes(self.confdir, self.config.html_theme_path, warn=self.warn) themename, themeoptions = self.get_theme_config() self.theme = Theme(themename) self.theme_options = themeoptions.copy() self.create_template_bridge() self.templates.init(self, self.theme) def init_highlighter(self): # determine Pygments style and create the highlighter if self.config.pygments_style is not None: style = self.config.pygments_style elif self.theme: style = self.theme.get_confstr('theme', 'pygments_style', 'none') else: style = 'sphinx' self.highlighter = PygmentsBridge('html', style, self.config.trim_doctest_flags) def init_translator_class(self): if self.config.html_translator_class: self.translator_class = self.app.import_object( self.config.html_translator_class, 'html_translator_class setting') elif self.config.html_use_smartypants: self.translator_class = SmartyPantsHTMLTranslator else: self.translator_class = HTMLTranslator def get_outdated_docs(self): cfgdict = dict((name, self.config[name]) for (name, desc) in self.config.values.items() if desc[1] == 'html') self.config_hash = get_stable_hash(cfgdict) self.tags_hash = get_stable_hash(sorted(self.tags)) old_config_hash = old_tags_hash = '' try: fp = open(path.join(self.outdir, '.buildinfo')) try: version = fp.readline() if version.rstrip() != '# Sphinx build info version 1': raise ValueError fp.readline() # skip commentary cfg, old_config_hash = fp.readline().strip().split(': ') if cfg != 'config': raise ValueError tag, old_tags_hash = fp.readline().strip().split(': ') if tag != 'tags': raise ValueError finally: fp.close() except ValueError: self.warn('unsupported build info format in %r, building all' % path.join(self.outdir, '.buildinfo')) except Exception: pass if old_config_hash != self.config_hash or \ old_tags_hash != self.tags_hash: for docname in self.env.found_docs: yield docname return if self.templates: template_mtime = self.templates.newest_template_mtime() else: template_mtime = 0 for docname in self.env.found_docs: if docname not in self.env.all_docs: yield docname continue targetname = self.get_outfilename(docname) try: targetmtime = path.getmtime(targetname) except Exception: targetmtime = 0 try: srcmtime = max(path.getmtime(self.env.doc2path(docname)), template_mtime) if srcmtime > targetmtime: yield docname except EnvironmentError: # source doesn't exist anymore pass def render_partial(self, node): """Utility: Render a lone doctree node.""" if node is None: return {'fragment': ''} doc = new_document(b('<partial node>')) doc.append(node) if self._publisher is None: self._publisher = Publisher(source_class=DocTreeInput, destination_class=StringOutput) self._publisher.set_components('standalone', 'restructuredtext', 'pseudoxml') pub = self._publisher pub.reader = DoctreeReader() pub.writer = HTMLWriter(self) pub.process_programmatic_settings(None, {'output_encoding': 'unicode'}, None) pub.set_source(doc, None) pub.set_destination(None, None) pub.publish() return pub.writer.parts def prepare_writing(self, docnames): # create the search indexer from sphinx.search import IndexBuilder, languages lang = self.config.html_search_language or self.config.language if not lang or lang not in languages: lang = 'en' self.indexer = IndexBuilder(self.env, lang, self.config.html_search_options, self.config.html_search_scorer) self.load_indexer(docnames) self.docwriter = HTMLWriter(self) self.docsettings = OptionParser( defaults=self.env.settings, components=(self.docwriter, ), read_config_files=True).get_default_values() self.docsettings.compact_lists = bool(self.config.html_compact_lists) # determine the additional indices to include self.domain_indices = [] # html_domain_indices can be False/True or a list of index names indices_config = self.config.html_domain_indices if indices_config: for domain in self.env.domains.values(): for indexcls in domain.indices: indexname = '%s-%s' % (domain.name, indexcls.name) if isinstance(indices_config, list): if indexname not in indices_config: continue # deprecated config value if indexname == 'py-modindex' and \ not self.config.html_use_modindex: continue content, collapse = indexcls(domain).generate() if content: self.domain_indices.append( (indexname, indexcls, content, collapse)) # format the "last updated on" string, only once is enough since it # typically doesn't include the time of day lufmt = self.config.html_last_updated_fmt if lufmt is not None: self.last_updated = ustrftime(lufmt or _('%b %d, %Y')) else: self.last_updated = None logo = self.config.html_logo and \ path.basename(self.config.html_logo) or '' favicon = self.config.html_favicon and \ path.basename(self.config.html_favicon) or '' if favicon and os.path.splitext(favicon)[1] != '.ico': self.warn('html_favicon is not an .ico file') if not isinstance(self.config.html_use_opensearch, str): self.warn('html_use_opensearch config value must now be a string') self.relations = self.env.collect_relations() rellinks = [] if self.get_builder_config('use_index', 'html'): rellinks.append(('genindex', _('General Index'), 'I', _('index'))) for indexname, indexcls, content, collapse in self.domain_indices: # if it has a short name if indexcls.shortname: rellinks.append( (indexname, indexcls.localname, '', indexcls.shortname)) if self.config.html_style is not None: stylename = self.config.html_style elif self.theme: stylename = self.theme.get_confstr('theme', 'stylesheet') else: stylename = 'default.css' self.globalcontext = dict( embedded=self.embedded, project=self.config.project, release=self.config.release, version=self.config.version, last_updated=self.last_updated, copyright=self.config.copyright, master_doc=self.config.master_doc, use_opensearch=self.config.html_use_opensearch, docstitle=self.config.html_title, shorttitle=self.config.html_short_title, show_copyright=self.config.html_show_copyright, show_sphinx=self.config.html_show_sphinx, has_source=self.config.html_copy_source, show_source=self.config.html_show_sourcelink, file_suffix=self.out_suffix, script_files=self.script_files, css_files=self.css_files, sphinx_version=__version__, style=stylename, rellinks=rellinks, builder=self.name, parents=[], logo=logo, favicon=favicon, ) if self.theme: self.globalcontext.update(('theme_' + key, val) for ( key, val) in self.theme.get_options(self.theme_options).items()) self.globalcontext.update(self.config.html_context) def get_doc_context(self, docname, body, metatags): """Collect items for the template context of a page.""" # find out relations prev = next = None parents = [] rellinks = self.globalcontext['rellinks'][:] related = self.relations.get(docname) titles = self.env.titles if related and related[2]: try: next = { 'link': self.get_relative_uri(docname, related[2]), 'title': self.render_partial(titles[related[2]])['title'] } rellinks.append((related[2], next['title'], 'N', _('next'))) except KeyError: next = None if related and related[1]: try: prev = { 'link': self.get_relative_uri(docname, related[1]), 'title': self.render_partial(titles[related[1]])['title'] } rellinks.append( (related[1], prev['title'], 'P', _('previous'))) except KeyError: # the relation is (somehow) not in the TOC tree, handle # that gracefully prev = None while related and related[0]: try: parents.append({ 'link': self.get_relative_uri(docname, related[0]), 'title': self.render_partial(titles[related[0]])['title'] }) except KeyError: pass related = self.relations.get(related[0]) if parents: parents.pop() # remove link to the master file; we have a generic # "back to index" link already parents.reverse() # title rendered as HTML title = self.env.longtitles.get(docname) title = title and self.render_partial(title)['title'] or '' # the name for the copied source sourcename = self.config.html_copy_source and docname + '.txt' or '' # metadata for the document meta = self.env.metadata.get(docname) # local TOC and global TOC tree self_toc = self.env.get_toc_for(docname, self) toc = self.render_partial(self_toc)['fragment'] return dict( parents=parents, prev=prev, next=next, title=title, meta=meta, body=body, metatags=metatags, rellinks=rellinks, sourcename=sourcename, toc=toc, # only display a TOC if there's more than one item to show display_toc=(self.env.toc_num_entries[docname] > 1), ) def write_doc(self, docname, doctree): destination = StringOutput(encoding='utf-8') doctree.settings = self.docsettings self.secnumbers = self.env.toc_secnumbers.get(docname, {}) self.imgpath = relative_uri(self.get_target_uri(docname), '_images') self.dlpath = relative_uri(self.get_target_uri(docname), '_downloads') self.current_docname = docname self.docwriter.write(doctree, destination) self.docwriter.assemble_parts() body = self.docwriter.parts['fragment'] metatags = self.docwriter.clean_meta ctx = self.get_doc_context(docname, body, metatags) self.handle_page(docname, ctx, event_arg=doctree) def write_doc_serialized(self, docname, doctree): self.imgpath = relative_uri(self.get_target_uri(docname), '_images') self.post_process_images(doctree) title = self.env.longtitles.get(docname) title = title and self.render_partial(title)['title'] or '' self.index_page(docname, doctree, title) def finish(self): self.info(bold('writing additional files...'), nonl=1) # pages from extensions for pagelist in self.app.emit('html-collect-pages'): for pagename, context, template in pagelist: self.handle_page(pagename, context, template) # the global general index if self.get_builder_config('use_index', 'html'): self.write_genindex() # the global domain-specific indices self.write_domain_indices() # the search page if self.name != 'htmlhelp': self.info(' search', nonl=1) self.handle_page('search', {}, 'search.html') # additional pages from conf.py for pagename, template in list( self.config.html_additional_pages.items()): self.info(' ' + pagename, nonl=1) self.handle_page(pagename, {}, template) if self.config.html_use_opensearch and self.name != 'htmlhelp': self.info(' opensearch', nonl=1) fn = path.join(self.outdir, '_static', 'opensearch.xml') self.handle_page('opensearch', {}, 'opensearch.xml', outfilename=fn) self.info() self.copy_image_files() self.copy_download_files() self.copy_static_files() self.copy_extra_files() self.write_buildinfo() # dump the search index self.handle_finish() def write_genindex(self): # the total count of lines for each index letter, used to distribute # the entries into two columns genindex = self.env.create_index(self) indexcounts = [] for _, entries in genindex: indexcounts.append( sum(1 + len(subitems) for _, (_, subitems) in entries)) genindexcontext = dict( genindexentries=genindex, genindexcounts=indexcounts, split_index=self.config.html_split_index, ) self.info(' genindex', nonl=1) if self.config.html_split_index: self.handle_page('genindex', genindexcontext, 'genindex-split.html') self.handle_page('genindex-all', genindexcontext, 'genindex.html') for (key, entries), count in zip(genindex, indexcounts): ctx = { 'key': key, 'entries': entries, 'count': count, 'genindexentries': genindex } self.handle_page('genindex-' + key, ctx, 'genindex-single.html') else: self.handle_page('genindex', genindexcontext, 'genindex.html') def write_domain_indices(self): for indexname, indexcls, content, collapse in self.domain_indices: indexcontext = dict( indextitle=indexcls.localname, content=content, collapse_index=collapse, ) self.info(' ' + indexname, nonl=1) self.handle_page(indexname, indexcontext, 'domainindex.html') def copy_image_files(self): # copy image files if self.images: ensuredir(path.join(self.outdir, '_images')) for src in self.status_iterator(self.images, 'copying images... ', brown, len(self.images)): dest = self.images[src] try: copyfile(path.join(self.srcdir, src), path.join(self.outdir, '_images', dest)) except Exception as err: self.warn('cannot copy image file %r: %s' % (path.join(self.srcdir, src), err)) def copy_download_files(self): # copy downloadable files if self.env.dlfiles: ensuredir(path.join(self.outdir, '_downloads')) for src in self.status_iterator(self.env.dlfiles, 'copying downloadable files... ', brown, len(self.env.dlfiles)): dest = self.env.dlfiles[src][1] try: copyfile(path.join(self.srcdir, src), path.join(self.outdir, '_downloads', dest)) except Exception as err: self.warn('cannot copy downloadable file %r: %s' % (path.join(self.srcdir, src), err)) def copy_static_files(self): # copy static files self.info(bold('copying static files... '), nonl=True) ensuredir(path.join(self.outdir, '_static')) # first, create pygments style file f = open(path.join(self.outdir, '_static', 'pygments.css'), 'w') f.write(self.highlighter.get_stylesheet()) f.close() # then, copy translations JavaScript file if self.config.language is not None: jsfile = self._get_translations_js() if jsfile: copyfile(jsfile, path.join(self.outdir, '_static', 'translations.js')) # add context items for search function used in searchtools.js_t ctx = self.globalcontext.copy() ctx.update(self.indexer.context_for_searchtool()) # then, copy over theme-supplied static files if self.theme: themeentries = [ path.join(themepath, 'static') for themepath in self.theme.get_dirchain()[::-1] ] for entry in themeentries: copy_static_entry(entry, path.join(self.outdir, '_static'), self, ctx) # then, copy over all user-supplied static files staticentries = [ path.join(self.confdir, spath) for spath in self.config.html_static_path ] matchers = compile_matchers( self.config.exclude_patterns + ['**/' + d for d in self.config.exclude_dirnames]) for entry in staticentries: if not path.exists(entry): self.warn('html_static_path entry %r does not exist' % entry) continue copy_static_entry(entry, path.join(self.outdir, '_static'), self, ctx, exclude_matchers=matchers) # copy logo and favicon files if not already in static path if self.config.html_logo: logobase = path.basename(self.config.html_logo) logotarget = path.join(self.outdir, '_static', logobase) if not path.isfile(logotarget): copyfile(path.join(self.confdir, self.config.html_logo), logotarget) if self.config.html_favicon: iconbase = path.basename(self.config.html_favicon) icontarget = path.join(self.outdir, '_static', iconbase) if not path.isfile(icontarget): copyfile(path.join(self.confdir, self.config.html_favicon), icontarget) self.info('done') def copy_extra_files(self): # copy html_extra_path files self.info(bold('copying extra files... '), nonl=True) extraentries = [ path.join(self.confdir, epath) for epath in self.config.html_extra_path ] for entry in extraentries: if not path.exists(entry): self.warn('html_extra_path entry %r does not exist' % entry) continue copy_static_entry(entry, self.outdir, self) def write_buildinfo(self): # write build info file fp = open(path.join(self.outdir, '.buildinfo'), 'w') try: fp.write('# Sphinx build info version 1\n' '# This file hashes the configuration used when building' ' these files. When it is not found, a full rebuild will' ' be done.\nconfig: %s\ntags: %s\n' % (self.config_hash, self.tags_hash)) finally: fp.close() def cleanup(self): # clean up theme stuff if self.theme: self.theme.cleanup() def post_process_images(self, doctree): """Pick the best candidate for an image and link down-scaled images to their high res version. """ Builder.post_process_images(self, doctree) for node in doctree.traverse(nodes.image): scale_keys = ('scale', 'width', 'height') if not any((key in node) for key in scale_keys) or \ isinstance(node.parent, nodes.reference): # docutils does unfortunately not preserve the # ``target`` attribute on images, so we need to check # the parent node here. continue uri = node['uri'] reference = nodes.reference('', '', internal=True) if uri in self.images: reference['refuri'] = posixpath.join(self.imgpath, self.images[uri]) else: reference['refuri'] = uri node.replace_self(reference) reference.append(node) def load_indexer(self, docnames): keep = set(self.env.all_docs) - set(docnames) try: searchindexfn = path.join(self.outdir, self.searchindex_filename) if self.indexer_dumps_unicode: f = codecs.open(searchindexfn, 'r', encoding='utf-8') else: f = open(searchindexfn, 'rb') try: self.indexer.load(f, self.indexer_format) finally: f.close() except (IOError, OSError, ValueError): if keep: self.warn('search index couldn\'t be loaded, but not all ' 'documents will be built: the index will be ' 'incomplete.') # delete all entries for files that will be rebuilt self.indexer.prune(keep) def index_page(self, pagename, doctree, title): # only index pages with title if self.indexer is not None and title: self.indexer.feed(pagename, title, doctree) def _get_local_toctree(self, docname, collapse=True, **kwds): if 'includehidden' not in kwds: kwds['includehidden'] = False return self.render_partial( self.env.get_toctree_for(docname, self, collapse, **kwds))['fragment'] def get_outfilename(self, pagename): return path.join(self.outdir, os_path(pagename) + self.out_suffix) def add_sidebars(self, pagename, ctx): def has_wildcard(pattern): return any(char in pattern for char in '*?[') sidebars = None matched = None customsidebar = None for pattern, patsidebars in self.config.html_sidebars.items(): if patmatch(pagename, pattern): if matched: if has_wildcard(pattern): # warn if both patterns contain wildcards if has_wildcard(matched): self.warn('page %s matches two patterns in ' 'html_sidebars: %r and %r' % (pagename, matched, pattern)) # else the already matched pattern is more specific # than the present one, because it contains no wildcard continue matched = pattern sidebars = patsidebars if sidebars is None: # keep defaults pass elif isinstance(sidebars, str): # 0.x compatible mode: insert custom sidebar before searchbox customsidebar = sidebars sidebars = None ctx['sidebars'] = sidebars ctx['customsidebar'] = customsidebar # --------- these are overwritten by the serialization builder def get_target_uri(self, docname, typ=None): return docname + self.link_suffix def handle_page(self, pagename, addctx, templatename='page.html', outfilename=None, event_arg=None): ctx = self.globalcontext.copy() # current_page_name is backwards compatibility ctx['pagename'] = ctx['current_page_name'] = pagename default_baseuri = self.get_target_uri(pagename) # in the singlehtml builder, default_baseuri still contains an #anchor # part, which relative_uri doesn't really like... default_baseuri = default_baseuri.rsplit('#', 1)[0] def pathto(otheruri, resource=False, baseuri=default_baseuri): if resource and '://' in otheruri: # allow non-local resources given by scheme return otheruri elif not resource: otheruri = self.get_target_uri(otheruri) uri = relative_uri(baseuri, otheruri) or '#' return uri ctx['pathto'] = pathto ctx['hasdoc'] = lambda name: name in self.env.all_docs if self.name != 'htmlhelp': ctx['encoding'] = encoding = self.config.html_output_encoding else: ctx['encoding'] = encoding = self.encoding ctx['toctree'] = lambda **kw: self._get_local_toctree(pagename, **kw) self.add_sidebars(pagename, ctx) ctx.update(addctx) self.app.emit('html-page-context', pagename, templatename, ctx, event_arg) try: output = self.templates.render(templatename, ctx) except UnicodeError: self.warn("a Unicode error occurred when rendering the page %s. " "Please make sure all config values that contain " "non-ASCII content are Unicode strings." % pagename) return if not outfilename: outfilename = self.get_outfilename(pagename) # outfilename's path is in general different from self.outdir ensuredir(path.dirname(outfilename)) try: f = codecs.open(outfilename, 'w', encoding, 'xmlcharrefreplace') try: f.write(output) finally: f.close() except (IOError, OSError) as err: self.warn("error writing file %s: %s" % (outfilename, err)) if self.copysource and ctx.get('sourcename'): # copy the source file for the "show source" link source_name = path.join(self.outdir, '_sources', os_path(ctx['sourcename'])) ensuredir(path.dirname(source_name)) copyfile(self.env.doc2path(pagename), source_name) def handle_finish(self): self.dump_search_index() self.dump_inventory() def dump_inventory(self): self.info(bold('dumping object inventory... '), nonl=True) f = open(path.join(self.outdir, INVENTORY_FILENAME), 'wb') try: f.write( ('# Sphinx inventory version 2\n' '# Project: %s\n' '# Version: %s\n' '# The remainder of this file is compressed using zlib.\n' % (self.config.project, self.config.version)).encode('utf-8')) compressor = zlib.compressobj(9) for domainname, domain in self.env.domains.items(): for name, dispname, type, docname, anchor, prio in \ domain.get_objects(): if anchor.endswith(name): # this can shorten the inventory by as much as 25% anchor = anchor[:-len(name)] + '$' uri = self.get_target_uri(docname) + '#' + anchor if dispname == name: dispname = '-' f.write( compressor.compress(('%s %s:%s %s %s %s\n' % (name, domainname, type, prio, uri, dispname)).encode('utf-8'))) f.write(compressor.flush()) finally: f.close() self.info('done') def dump_search_index(self): self.info(bold('dumping search index... '), nonl=True) self.indexer.prune(self.env.all_docs) searchindexfn = path.join(self.outdir, self.searchindex_filename) # first write to a temporary file, so that if dumping fails, # the existing index won't be overwritten if self.indexer_dumps_unicode: f = codecs.open(searchindexfn + '.tmp', 'w', encoding='utf-8') else: f = open(searchindexfn + '.tmp', 'wb') try: self.indexer.dump(f, self.indexer_format) finally: f.close() movefile(searchindexfn + '.tmp', searchindexfn) self.info('done')
class StandaloneHTMLBuilder(Builder): """ Builds standalone HTML docs. """ name = 'html' format = 'html' copysource = True allow_parallel = True out_suffix = '.html' link_suffix = '.html' # defaults to matching out_suffix indexer_format = js_index indexer_dumps_unicode = True supported_image_types = ['image/svg+xml', 'image/png', 'image/gif', 'image/jpeg'] searchindex_filename = 'searchindex.js' add_permalinks = True embedded = False # for things like HTML help or Qt help: suppresses sidebar # This is a class attribute because it is mutated by Sphinx.add_javascript. script_files = ['_static/jquery.js', '_static/underscore.js', '_static/doctools.js'] # Dito for this one. css_files = [] default_sidebars = ['localtoc.html', 'relations.html', 'sourcelink.html', 'searchbox.html'] # cached publisher object for snippets _publisher = None def init(self): # a hash of all config values that, if changed, cause a full rebuild self.config_hash = '' self.tags_hash = '' # section numbers for headings in the currently visited document self.secnumbers = {} # currently written docname self.current_docname = None self.init_templates() self.init_highlighter() self.init_translator_class() if self.config.html_file_suffix is not None: self.out_suffix = self.config.html_file_suffix if self.config.html_link_suffix is not None: self.link_suffix = self.config.html_link_suffix else: self.link_suffix = self.out_suffix if self.config.language is not None: if self._get_translations_js(): self.script_files.append('_static/translations.js') def _get_translations_js(self): candidates = [path.join(package_dir, 'locale', self.config.language, 'LC_MESSAGES', 'sphinx.js'), path.join(sys.prefix, 'share/sphinx/locale', self.config.language, 'sphinx.js')] + \ [path.join(dir, self.config.language, 'LC_MESSAGES', 'sphinx.js') for dir in self.config.locale_dirs] for jsfile in candidates: if path.isfile(jsfile): return jsfile return None def get_theme_config(self): return self.config.html_theme, self.config.html_theme_options def init_templates(self): Theme.init_themes(self.confdir, self.config.html_theme_path, warn=self.warn) themename, themeoptions = self.get_theme_config() self.theme = Theme(themename) self.theme_options = themeoptions.copy() self.create_template_bridge() self.templates.init(self, self.theme) def init_highlighter(self): # determine Pygments style and create the highlighter if self.config.pygments_style is not None: style = self.config.pygments_style elif self.theme: style = self.theme.get_confstr('theme', 'pygments_style', 'none') else: style = 'sphinx' self.highlighter = PygmentsBridge('html', style, self.config.trim_doctest_flags) def init_translator_class(self): if self.config.html_translator_class: self.translator_class = self.app.import_object( self.config.html_translator_class, 'html_translator_class setting') elif self.config.html_use_smartypants: self.translator_class = SmartyPantsHTMLTranslator else: self.translator_class = HTMLTranslator def get_outdated_docs(self): cfgdict = dict((name, self.config[name]) for (name, desc) in self.config.values.iteritems() if desc[1] == 'html') self.config_hash = get_stable_hash(cfgdict) self.tags_hash = get_stable_hash(sorted(self.tags)) old_config_hash = old_tags_hash = '' try: fp = open(path.join(self.outdir, '.buildinfo')) try: version = fp.readline() if version.rstrip() != '# Sphinx build info version 1': raise ValueError fp.readline() # skip commentary cfg, old_config_hash = fp.readline().strip().split(': ') if cfg != 'config': raise ValueError tag, old_tags_hash = fp.readline().strip().split(': ') if tag != 'tags': raise ValueError finally: fp.close() except ValueError: self.warn('unsupported build info format in %r, building all' % path.join(self.outdir, '.buildinfo')) except Exception: pass if old_config_hash != self.config_hash or \ old_tags_hash != self.tags_hash: for docname in self.env.found_docs: yield docname return if self.templates: template_mtime = self.templates.newest_template_mtime() else: template_mtime = 0 for docname in self.env.found_docs: if docname not in self.env.all_docs: yield docname continue targetname = self.get_outfilename(docname) try: targetmtime = path.getmtime(targetname) except Exception: targetmtime = 0 try: srcmtime = max(path.getmtime(self.env.doc2path(docname)), template_mtime) if srcmtime > targetmtime: yield docname except EnvironmentError: # source doesn't exist anymore pass def render_partial(self, node): """Utility: Render a lone doctree node.""" if node is None: return {'fragment': ''} doc = new_document(b('<partial node>')) doc.append(node) if self._publisher is None: self._publisher = Publisher( source_class = DocTreeInput, destination_class=StringOutput) self._publisher.set_components('standalone', 'restructuredtext', 'pseudoxml') pub = self._publisher pub.reader = DoctreeReader() pub.writer = HTMLWriter(self) pub.process_programmatic_settings( None, {'output_encoding': 'unicode'}, None) pub.set_source(doc, None) pub.set_destination(None, None) pub.publish() return pub.writer.parts def prepare_writing(self, docnames): # create the search indexer from sphinx.search import IndexBuilder, languages lang = self.config.html_search_language or self.config.language if not lang or lang not in languages: lang = 'en' self.indexer = IndexBuilder(self.env, lang, self.config.html_search_options, self.config.html_search_scorer) self.load_indexer(docnames) self.docwriter = HTMLWriter(self) self.docsettings = OptionParser( defaults=self.env.settings, components=(self.docwriter,), read_config_files=True).get_default_values() self.docsettings.compact_lists = bool(self.config.html_compact_lists) # determine the additional indices to include self.domain_indices = [] # html_domain_indices can be False/True or a list of index names indices_config = self.config.html_domain_indices if indices_config: for domain in self.env.domains.itervalues(): for indexcls in domain.indices: indexname = '%s-%s' % (domain.name, indexcls.name) if isinstance(indices_config, list): if indexname not in indices_config: continue # deprecated config value if indexname == 'py-modindex' and \ not self.config.html_use_modindex: continue content, collapse = indexcls(domain).generate() if content: self.domain_indices.append( (indexname, indexcls, content, collapse)) # format the "last updated on" string, only once is enough since it # typically doesn't include the time of day lufmt = self.config.html_last_updated_fmt if lufmt is not None: self.last_updated = ustrftime(lufmt or _('%b %d, %Y')) else: self.last_updated = None logo = self.config.html_logo and \ path.basename(self.config.html_logo) or '' favicon = self.config.html_favicon and \ path.basename(self.config.html_favicon) or '' if favicon and os.path.splitext(favicon)[1] != '.ico': self.warn('html_favicon is not an .ico file') if not isinstance(self.config.html_use_opensearch, basestring): self.warn('html_use_opensearch config value must now be a string') self.relations = self.env.collect_relations() rellinks = [] if self.get_builder_config('use_index', 'html'): rellinks.append(('genindex', _('General Index'), 'I', _('index'))) for indexname, indexcls, content, collapse in self.domain_indices: # if it has a short name if indexcls.shortname: rellinks.append((indexname, indexcls.localname, '', indexcls.shortname)) if self.config.html_style is not None: stylename = self.config.html_style elif self.theme: stylename = self.theme.get_confstr('theme', 'stylesheet') else: stylename = 'default.css' self.globalcontext = dict( embedded = self.embedded, project = self.config.project, release = self.config.release, version = self.config.version, last_updated = self.last_updated, copyright = self.config.copyright, master_doc = self.config.master_doc, use_opensearch = self.config.html_use_opensearch, docstitle = self.config.html_title, shorttitle = self.config.html_short_title, show_copyright = self.config.html_show_copyright, show_sphinx = self.config.html_show_sphinx, has_source = self.config.html_copy_source, show_source = self.config.html_show_sourcelink, file_suffix = self.out_suffix, script_files = self.script_files, css_files = self.css_files, sphinx_version = __version__, style = stylename, rellinks = rellinks, builder = self.name, parents = [], logo = logo, favicon = favicon, ) if self.theme: self.globalcontext.update( ('theme_' + key, val) for (key, val) in self.theme.get_options(self.theme_options).iteritems()) self.globalcontext.update(self.config.html_context) def get_doc_context(self, docname, body, metatags): """Collect items for the template context of a page.""" # find out relations prev = next = None parents = [] rellinks = self.globalcontext['rellinks'][:] related = self.relations.get(docname) titles = self.env.titles if related and related[2]: try: next = { 'link': self.get_relative_uri(docname, related[2]), 'title': self.render_partial(titles[related[2]])['title'] } rellinks.append((related[2], next['title'], 'N', _('next'))) except KeyError: next = None if related and related[1]: try: prev = { 'link': self.get_relative_uri(docname, related[1]), 'title': self.render_partial(titles[related[1]])['title'] } rellinks.append((related[1], prev['title'], 'P', _('previous'))) except KeyError: # the relation is (somehow) not in the TOC tree, handle # that gracefully prev = None while related and related[0]: try: parents.append( {'link': self.get_relative_uri(docname, related[0]), 'title': self.render_partial(titles[related[0]])['title']}) except KeyError: pass related = self.relations.get(related[0]) if parents: parents.pop() # remove link to the master file; we have a generic # "back to index" link already parents.reverse() # title rendered as HTML title = self.env.longtitles.get(docname) title = title and self.render_partial(title)['title'] or '' # the name for the copied source sourcename = self.config.html_copy_source and docname + '.txt' or '' # metadata for the document meta = self.env.metadata.get(docname) # local TOC and global TOC tree self_toc = self.env.get_toc_for(docname, self) toc = self.render_partial(self_toc)['fragment'] return dict( parents = parents, prev = prev, next = next, title = title, meta = meta, body = body, metatags = metatags, rellinks = rellinks, sourcename = sourcename, toc = toc, # only display a TOC if there's more than one item to show display_toc = (self.env.toc_num_entries[docname] > 1), ) def write_doc(self, docname, doctree): destination = StringOutput(encoding='utf-8') doctree.settings = self.docsettings self.secnumbers = self.env.toc_secnumbers.get(docname, {}) self.imgpath = relative_uri(self.get_target_uri(docname), '_images') self.dlpath = relative_uri(self.get_target_uri(docname), '_downloads') self.current_docname = docname self.docwriter.write(doctree, destination) self.docwriter.assemble_parts() body = self.docwriter.parts['fragment'] metatags = self.docwriter.clean_meta ctx = self.get_doc_context(docname, body, metatags) self.handle_page(docname, ctx, event_arg=doctree) def write_doc_serialized(self, docname, doctree): self.imgpath = relative_uri(self.get_target_uri(docname), '_images') self.post_process_images(doctree) title = self.env.longtitles.get(docname) title = title and self.render_partial(title)['title'] or '' self.index_page(docname, doctree, title) def finish(self): self.info(bold('writing additional files...'), nonl=1) # pages from extensions for pagelist in self.app.emit('html-collect-pages'): for pagename, context, template in pagelist: self.handle_page(pagename, context, template) # the global general index if self.get_builder_config('use_index', 'html'): self.write_genindex() # the global domain-specific indices self.write_domain_indices() # the search page if self.name != 'htmlhelp': self.info(' search', nonl=1) self.handle_page('search', {}, 'search.html') # additional pages from conf.py for pagename, template in self.config.html_additional_pages.items(): self.info(' '+pagename, nonl=1) self.handle_page(pagename, {}, template) if self.config.html_use_opensearch and self.name != 'htmlhelp': self.info(' opensearch', nonl=1) fn = path.join(self.outdir, '_static', 'opensearch.xml') self.handle_page('opensearch', {}, 'opensearch.xml', outfilename=fn) self.info() self.copy_image_files() self.copy_download_files() self.copy_static_files() self.copy_extra_files() self.write_buildinfo() # dump the search index self.handle_finish() def write_genindex(self): # the total count of lines for each index letter, used to distribute # the entries into two columns genindex = self.env.create_index(self) indexcounts = [] for _, entries in genindex: indexcounts.append(sum(1 + len(subitems) for _, (_, subitems) in entries)) genindexcontext = dict( genindexentries = genindex, genindexcounts = indexcounts, split_index = self.config.html_split_index, ) self.info(' genindex', nonl=1) if self.config.html_split_index: self.handle_page('genindex', genindexcontext, 'genindex-split.html') self.handle_page('genindex-all', genindexcontext, 'genindex.html') for (key, entries), count in zip(genindex, indexcounts): ctx = {'key': key, 'entries': entries, 'count': count, 'genindexentries': genindex} self.handle_page('genindex-' + key, ctx, 'genindex-single.html') else: self.handle_page('genindex', genindexcontext, 'genindex.html') def write_domain_indices(self): for indexname, indexcls, content, collapse in self.domain_indices: indexcontext = dict( indextitle = indexcls.localname, content = content, collapse_index = collapse, ) self.info(' ' + indexname, nonl=1) self.handle_page(indexname, indexcontext, 'domainindex.html') def copy_image_files(self): # copy image files if self.images: ensuredir(path.join(self.outdir, '_images')) for src in self.status_iterator(self.images, 'copying images... ', brown, len(self.images)): dest = self.images[src] try: copyfile(path.join(self.srcdir, src), path.join(self.outdir, '_images', dest)) except Exception, err: self.warn('cannot copy image file %r: %s' % (path.join(self.srcdir, src), err))
def get_doctree(path, **kwargs): """ Obtain a Sphinx doctree from the RST file at ``path``. Performs no Releases-specific processing; this code would, ideally, be in Sphinx itself, but things there are pretty tightly coupled. So we wrote this. Any additional kwargs are passed unmodified into an internal `make_app` call. :param str path: A relative or absolute file path string. :returns: A two-tuple of the generated ``sphinx.application.Sphinx`` app and the doctree (a ``docutils.document`` object). .. versionchanged:: 1.6 Added support for passing kwargs to `make_app`. """ root, filename = os.path.split(path) docname, _ = os.path.splitext(filename) # TODO: this only works for top level changelog files (i.e. ones where # their dirname is the project/doc root) app = make_app(srcdir=root, **kwargs) # Create & init a BuildEnvironment. Mm, tasty side effects. app._init_env(freshenv=True) env = app.env # More arity/API changes: Sphinx 1.3/1.4-ish require one to pass in the app # obj in BuildEnvironment.update(); modern Sphinx performs that inside # Application._init_env() (which we just called above) and so that kwarg is # removed from update(). EAFP. kwargs = dict( config=app.config, srcdir=root, doctreedir=app.doctreedir, app=app, ) try: env.update(**kwargs) except TypeError: # Assume newer Sphinx w/o an app= kwarg del kwargs['app'] env.update(**kwargs) # Code taken from sphinx.environment.read_doc; easier to manually call # it with a working Environment object, instead of doing more random crap # to trick the higher up build system into thinking our single changelog # document was "updated". env.temp_data['docname'] = docname env.app = app # NOTE: SphinxStandaloneReader API changed in 1.4 :( reader_kwargs = { 'app': app, 'parsers': env.config.source_parsers, } if sphinx.version_info[:2] < (1, 4): del reader_kwargs['app'] # This monkeypatches (!!!) docutils to 'inject' all registered Sphinx # domains' roles & so forth. Without this, rendering the doctree lacks # almost all Sphinx magic, including things like :ref: and :doc:! with sphinx_domains(env): reader = SphinxStandaloneReader(**reader_kwargs) pub = Publisher(reader=reader, writer=SphinxDummyWriter(), destination_class=NullOutput) pub.set_components(None, 'restructuredtext', None) pub.process_programmatic_settings(None, env.settings, None) # NOTE: docname derived higher up, from our given path src_path = env.doc2path(docname) source = SphinxFileInput( app, env, source=None, source_path=src_path, encoding=env.config.source_encoding, ) pub.source = source pub.settings._source = src_path pub.set_destination(None, None) pub.publish() return app, pub.document
def process_targets(site, logger, source, permalink): """Process the target locations in the reST files.""" site.processing_targets = True reader = Reader() reader.l_settings = {"source": source} with open(source, "r", encoding="utf8") as in_file: data = in_file.read() pub = Publisher( reader=reader, parser=None, writer=None, settings=None, source_class=StringInput, destination_class=StringOutput, ) pub.set_components(None, "restructuredtext", "html") # Reading the file will generate output/errors that we don't care about # at this stage. The report_level = 5 means no output pub.process_programmatic_settings(settings_spec=None, settings_overrides={"report_level": 5}, config_section=None) pub.set_source(data, None) pub.set_destination(None, None) pub.publish() document = pub.document site.processing_targets = False # Code based on Sphinx std domain for name, is_explicit in document.nametypes.items(): if not is_explicit: continue labelid = document.nameids[name] if labelid is None: continue node = document.ids[labelid] if node.tagname == "target" and "refid" in node: node = document.ids.get(node["refid"]) labelid = node["names"][0] if (node.tagname == "footnote" or "refuri" in node or node.tagname.startswith("desc_")): continue if name in site.ref_targets: logger.warn( "Duplicate label {dup}, other instance in {other}".format( dup=name, other=site.ref_targets[name][0])) site.anon_ref_targets[name] = permalink, labelid def clean_astext(node): """Like node.astext(), but ignore images. Taken from sphinx.util.nodes """ node = node.deepcopy() for img in node.traverse(nodes.image): img["alt"] = "" for raw in node.traverse(nodes.raw): raw.parent.remove(raw) return node.astext() if node.tagname in ("section", "rubric"): sectname = clean_astext(node[0]) else: continue site.ref_targets[name] = permalink, labelid, sectname
class RstPublisher: ############################################## def __init__(self): self._publisher = Publisher(source_class=StringInput, destination_class=StringOutput) self._publisher.set_components('standalone', 'restructuredtext', 'html') self._publisher.writer.translator_class = MyHTMLTranslator self._publisher.process_programmatic_settings(None, extra_params, None) ############################################## def init_plugins(self, plugins): site.addsitedir(str(PurePath(__file__).parent / 'RstPlugins')) for plugin in plugins: print('Register {}'.format(plugin)) plugin = __import__(plugin, globals(), locals(), str('module')) plugin.register(self) ############################################## def register_directive(self, directive_name, directive_class): directives.register_directive(directive_name, directive_class) ############################################## def register_role(self, role_name, role_class): roles.register_local_role(role_name, role_class) ############################################## def register_node(self, node_class, visit, depart): print(node_class.__name__, visit, depart) setattr(MyHTMLTranslator, 'visit_' + node_class.__name__, visit) setattr(MyHTMLTranslator, 'depart_' + node_class.__name__, depart) ############################################## def publish(self, source, header_level=None, report_level=None): settings_overrides = DOCUTILS_RENDERER_SETTINGS.copy() if header_level is not None: # starts from 1 settings_overrides['initial_header_level'] = header_level if report_level is not None: # starts from 1 too settings_overrides['report_level'] = 0 # report_level self._publisher.set_source(source=source) self._publisher.publish(enable_exit_status=True) parts = self._publisher.writer.parts return parts[ 'html_body'] # parts['body_pre_docinfo'] + parts['fragment']
def lint(content, filepath=None): """Lint reStructuredText and return errors :param string content: reStructuredText to be linted :param string filepath: Optional path to file, this will be returned as the source :rtype list: List of errors. Each error will contain a line, source (filepath), message (error message), and full message (error message + source lines) """ # Generate a new parser (copying `rst2html.py` flow) # http://repo.or.cz/w/docutils.git/blob/422cede485668203abc01c76ca317578ff634b30:/docutils/tools/rst2html.py # http://repo.or.cz/w/docutils.git/blob/422cede485668203abc01c76ca317578ff634b30:/docutils/docutils/core.py#l348 pub = Publisher(None, None, None, settings=None) pub.set_components('standalone', 'restructuredtext', 'pseudoxml') # Configure publisher # DEV: We cannot use `process_command_line` since it processes `sys.argv` which is for `rst-lint`, not `docutils` # http://repo.or.cz/w/docutils.git/blob/422cede485668203abc01c76ca317578ff634b30:/docutils/docutils/core.py#l201 # http://repo.or.cz/w/docutils.git/blob/422cede485668203abc01c76ca317578ff634b30:/docutils/docutils/core.py#l143 # http://repo.or.cz/w/docutils.git/blob/422cede485668203abc01c76ca317578ff634b30:/docutils/docutils/core.py#l118 settings = pub.get_settings(halt_level=5) pub.set_io() # Prepare a document to parse on # DEV: We avoid the `read` method because when `source` is `None`, it attempts to read from `stdin`. # However, we already know our content. # DEV: We create our document without `parse` because we need to attach observer's before parsing # http://repo.or.cz/w/docutils.git/blob/422cede485668203abc01c76ca317578ff634b30:/docutils/docutils/readers/__init__.py#l66 reader = pub.reader document = utils.new_document(filepath, settings) # Disable stdout # TODO: Find a more proper way to do this # TODO: We might exit the program if a certain error level is reached document.reporter.stream = None # Collect errors via an observer errors = [] def error_collector(data): # Mutate the data since it was just generated data.line = data.get('line') data.source = data['source'] data.level = data['level'] data.type = data['type'] data.message = Element.astext(data.children[0]) data.full_message = Element.astext(data) # Save the error errors.append(data) document.reporter.attach_observer(error_collector) # Parse the content (and collect errors) # http://repo.or.cz/w/docutils.git/blob/422cede485668203abc01c76ca317578ff634b30:/docutils/docutils/readers/__init__.py#l75 reader.parser.parse(content, document) # Apply transforms (and more collect errors) # DEV: We cannot use `apply_transforms` since it has `attach_observer` baked in. We want only our listener. # http://repo.or.cz/w/docutils.git/blob/422cede485668203abc01c76ca317578ff634b30:/docutils/docutils/core.py#l195 # http://repo.or.cz/w/docutils.git/blob/422cede485668203abc01c76ca317578ff634b30:/docutils/docutils/transforms/__init__.py#l159 document.transformer.populate_from_components( (pub.source, pub.reader, pub.reader.parser, pub.writer, pub.destination)) transformer = document.transformer while transformer.transforms: if not transformer.sorted: # Unsorted initially, and whenever a transform is added. transformer.transforms.sort() transformer.transforms.reverse() transformer.sorted = 1 priority, transform_class, pending, kwargs = transformer.transforms.pop( ) transform = transform_class(transformer.document, startnode=pending) transform.apply(**kwargs) transformer.applied.append( (priority, transform_class, pending, kwargs)) return errors
from docutils.core import publish_cmdline, default_description,\ publish_doctree,\ Publisher from dotmpe.du.ext.writer.formresults import Writer as FormResultsWriter #publish_cmdline(reader=FormReader(), writer_name='pseudoxml')#writer=xhtmlform.Writer()) source_path = sys.argv[1] source = open(source_path) #doctree = publish_doctree(source, reader=FormReader())#, writer_name='pseudoxml') #print doctree pub = Publisher(FormReader(), writer=FormResultsWriter(), source_class=io.FileInput, destination_class=io.StringOutput)#, parser, writer, settings=settings, # source_class=source_class, # destination_class=destination_class) pub.set_components(None, 'rst', 'pseudoxml') pub.process_programmatic_settings(None, None, None) # settings_spec, settings_overrides, config_section) pub.set_source(source, source_path) pub.set_destination(None, None)#destination, destination_path) output = pub.publish(enable_exit_status=False) #print pub.document.form_processor.fields #print pub.document.form_processor.messages #print pub.document.form_processor.values print output
writer_name = 'html' else: writer = myWriter writer_name = None settings = None settings_spec = settings_spec settings_overrides = None config_section = None enable_exit_status = 1 argv = None usage = default_usage description = default_description pub = Publisher(reader, parser, writer, settings) pub.set_components(reader_name, parser_name, writer_name) if pub.settings is None: pub.process_command_line( argv, usage, description, settings_spec, config_section, **(settings_overrides or {})) output = pub.publish( argv, usage, description, settings_spec,
class MarkdownBuilder(Builder): """ Builds standalone Markdown docs. """ name = "markdown" format = "markdown" copysource = True allow_parallel = True out_suffix = ".md" link_suffix = ".md" # defaults to matching out_suffix # indexer_format = js_index indexer_dumps_unicode = True supported_image_types = ["image/svg+xml", "image/png", "image/gif", "image/jpeg"] # searchindex_filename = 'searchindex.js' add_permalinks = True embedded = False # for things like HTML help or Qt help: suppresses sidebar search = False # for things like HTML help and Apple help: suppress search # This is a class attribute because it is mutated by Sphinx.add_javascript. # script_files = ['_static/jquery.js', '_static/underscore.js', # '_static/doctools.js'] # Dito for this one. # css_files = [] # default_sidebars = ['localtoc.html', 'relations.html', # 'sourcelink.html', 'searchbox.html'] # cached publisher object for snippets _publisher = None def init(self): # a hash of all config values that, if changed, cause a full rebuild self.config_hash = "" self.tags_hash = "" # basename of images directory self.imagedir = "_images" # section numbers for headings in the currently visited document self.secnumbers = {} # currently written docname self.current_docname = None self.translator_class = MarkdownTranslator if self.config.html_file_suffix is not None: self.out_suffix = self.config.html_file_suffix if self.config.html_link_suffix is not None: self.link_suffix = self.config.html_link_suffix else: self.link_suffix = self.out_suffix self.create_template_bridge() self.templates.init(self, dirs="./") def get_target_uri(self, docname, typ=None): if docname == "index": return "" if docname.endswith(SEP + "index"): return docname[:-5] # up to sep return docname + SEP def get_outfilename(self, pagename): if pagename == "index" or pagename.endswith(SEP + "index"): outfilename = path.join(self.outdir, os_path(pagename) + self.out_suffix) else: outfilename = path.join(self.outdir, os_path(pagename), "index" + self.out_suffix) return outfilename # def _get_translations_js(self): # candidates = [path.join(package_dir, 'locale', self.config.language, # 'LC_MESSAGES', 'sphinx.js'), # path.join(sys.prefix, 'share/sphinx/locale', # self.config.language, 'sphinx.js')] + \ # [path.join(dir, self.config.language, # 'LC_MESSAGES', 'sphinx.js') # for dir in self.config.locale_dirs] # for jsfile in candidates: # if path.isfile(jsfile): # return jsfile # return None # def init_translator_class(self): # if self.translator_class is not None: # pass # elif self.config.html_translator_class: # self.translator_class = self.app.import_object( # self.config.html_translator_class, # 'html_translator_class setting') # elif self.config.html_use_smartypants: # self.translator_class = SmartyPantsHTMLTranslator # else: # self.translator_class = HTMLTranslator def get_outdated_docs(self): cfgdict = dict((name, self.config[name]) for (name, desc) in iteritems(self.config.values) if desc[1] == "html") self.config_hash = get_stable_hash(cfgdict) self.tags_hash = get_stable_hash(sorted(self.tags)) old_config_hash = old_tags_hash = "" try: fp = open(path.join(self.outdir, ".buildinfo")) try: version = fp.readline() if version.rstrip() != "# Sphinx build info version 1": raise ValueError fp.readline() # skip commentary cfg, old_config_hash = fp.readline().strip().split(": ") if cfg != "config": raise ValueError tag, old_tags_hash = fp.readline().strip().split(": ") if tag != "tags": raise ValueError finally: fp.close() except ValueError: self.warn("unsupported build info format in %r, building all" % path.join(self.outdir, ".buildinfo")) except Exception: pass if old_config_hash != self.config_hash or old_tags_hash != self.tags_hash: for docname in self.env.found_docs: yield docname return if self.templates: template_mtime = self.templates.newest_template_mtime() else: template_mtime = 0 for docname in self.env.found_docs: if docname not in self.env.all_docs: yield docname continue targetname = self.get_outfilename(docname) try: targetmtime = path.getmtime(targetname) except Exception: targetmtime = 0 try: srcmtime = max(path.getmtime(self.env.doc2path(docname)), template_mtime) if srcmtime > targetmtime: yield docname except EnvironmentError: # source doesn't exist anymore pass def render_partial(self, node): """Utility: Render a lone doctree node.""" if node is None: return {"fragment": ""} doc = new_document(b"<partial node>") doc.append(node) if self._publisher is None: self._publisher = Publisher(source_class=DocTreeInput, destination_class=StringOutput) self._publisher.set_components("standalone", "restructuredtext", "pseudoxml") pub = self._publisher pub.reader = DoctreeReader() pub.writer = MarkdownWriter(self) pub.process_programmatic_settings(None, {}, None) # pub.process_programmatic_settings( # None, {'output_encoding': 'unicode'}, None) pub.set_source(doc, None) pub.set_destination(None, None) pub.publish() return pub.writer.parts def prepare_writing(self, docnames): # create the search indexer self.indexer = None self.docwriter = MarkdownWriter(self) self.docsettings = OptionParser( defaults=self.env.settings, components=(self.docwriter,), read_config_files=True ).get_default_values() self.docsettings.compact_lists = bool(self.config.html_compact_lists) # determine the additional indices to include self.domain_indices = [] # html_domain_indices can be False/True or a list of index names indices_config = self.config.html_domain_indices if indices_config: for domain_name in sorted(self.env.domains): domain = self.env.domains[domain_name] for indexcls in domain.indices: indexname = "%s-%s" % (domain.name, indexcls.name) if isinstance(indices_config, list): if indexname not in indices_config: continue # deprecated config value if indexname == "py-modindex" and not self.config.html_use_modindex: continue content, collapse = indexcls(domain).generate() if content: self.domain_indices.append((indexname, indexcls, content, collapse)) # format the "last updated on" string, only once is enough since it # typically doesn't include the time of day lufmt = self.config.html_last_updated_fmt if lufmt is not None: self.last_updated = format_date(lufmt or _("%b %d, %Y"), language=self.config.language, warn=self.warn) else: self.last_updated = None self.relations = self.env.collect_relations() rellinks = [] if self.get_builder_config("use_index", "html"): rellinks.append(("genindex", _("General Index"), "I", _("index"))) for indexname, indexcls, content, collapse in self.domain_indices: # if it has a short name if indexcls.shortname: rellinks.append((indexname, indexcls.localname, "", indexcls.shortname)) self.globalcontext = dict( embedded=self.embedded, project=self.config.project, release=self.config.release, version=self.config.version, last_updated=self.last_updated, copyright=self.config.copyright, master_doc=self.config.master_doc, docstitle=self.config.html_title, shorttitle=self.config.html_short_title, show_copyright=self.config.html_show_copyright, show_sphinx=self.config.html_show_sphinx, has_source=self.config.html_copy_source, show_source=self.config.html_show_sourcelink, file_suffix=self.out_suffix, language=self.config.language, sphinx_version=__display_version__, rellinks=rellinks, builder=self.name, parents=[], ) def get_doc_context(self, docname, body): """Collect items for the template context of a page.""" # find out relations prev = next = None parents = [] rellinks = self.globalcontext["rellinks"][:] related = self.relations.get(docname) titles = self.env.titles if related and related[2]: try: next = { "link": self.get_relative_uri(docname, related[2]), "title": self.render_partial(titles[related[2]])["title"], } rellinks.append((related[2], next["title"], "N", _("next"))) except KeyError: next = None if related and related[1]: try: prev = { "link": self.get_relative_uri(docname, related[1]), "title": self.render_partial(titles[related[1]])["title"], } rellinks.append((related[1], prev["title"], "P", _("previous"))) except KeyError: # the relation is (somehow) not in the TOC tree, handle # that gracefully prev = None while related and related[0]: try: parents.append( { "link": self.get_relative_uri(docname, related[0]), "title": self.render_partial(titles[related[0]])["title"], } ) except KeyError: pass related = self.relations.get(related[0]) if parents: # remove link to the master file; we have a generic # "back to index" link already parents.pop() parents.reverse() # title rendered as HTML title = self.env.longtitles.get(docname) title = title and self.render_partial(title)["title"] or "" # the name for the copied source sourcename = self.config.html_copy_source and docname + ".txt" or "" # metadata for the document meta = self.env.metadata.get(docname) # Suffix for the document source_suffix = "." + self.env.doc2path(docname).split(".")[-1] # local TOC and global TOC tree self_toc = self.env.get_toc_for(docname, self) toc = self.render_partial(self_toc)["fragment"] return dict( parents=parents, prev=prev, next=next, title=title, meta=meta, body=body, rellinks=rellinks, sourcename=sourcename, toc=toc, # only display a TOC if there's more than one item to show display_toc=(self.env.toc_num_entries[docname] > 1), page_source_suffix=source_suffix, ) def write_doc(self, docname, doctree): destination = StringOutput(encoding="utf-8") doctree.settings = self.docsettings self.secnumbers = self.env.toc_secnumbers.get(docname, {}) self.fignumbers = self.env.toc_fignumbers.get(docname, {}) self.imgpath = relative_uri(self.get_target_uri(docname), "_images") self.dlpath = relative_uri(self.get_target_uri(docname), "_downloads") self.current_docname = docname self.docwriter.write(doctree, destination) self.docwriter.assemble_parts() body = self.docwriter.parts["fragment"] ctx = self.get_doc_context(docname, body) self.handle_page(docname, ctx, event_arg=doctree) def write_doc_serialized(self, docname, doctree): self.imgpath = relative_uri(self.get_target_uri(docname), self.imagedir) self.post_process_images(doctree) title = self.env.longtitles.get(docname) title = title and self.render_partial(title)["title"] or "" self.index_page(docname, doctree, title) def finish(self): self.finish_tasks.add_task(self.gen_indices) self.finish_tasks.add_task(self.gen_additional_pages) self.finish_tasks.add_task(self.copy_image_files) self.finish_tasks.add_task(self.copy_download_files) self.finish_tasks.add_task(self.copy_static_files) self.finish_tasks.add_task(self.copy_extra_files) self.finish_tasks.add_task(self.write_buildinfo) # dump the search index self.handle_finish() def gen_indices(self): self.info(bold("generating indices..."), nonl=1) # the global general index # if self.get_builder_config('use_index', 'html'): # self.write_genindex() # the global domain-specific indices self.write_domain_indices() self.info() def gen_additional_pages(self): # pages from extensions for pagelist in self.app.emit("html-collect-pages"): for pagename, context, template in pagelist: self.handle_page(pagename, context, template) self.info(bold("writing additional pages..."), nonl=1) # additional pages from conf.py for pagename, template in self.config.html_additional_pages.items(): self.info(" " + pagename, nonl=1) self.handle_page(pagename, {}, template) # the search page if self.search: self.info(" search", nonl=1) self.handle_page("search", {}, "search.html") # the opensearch xml file if self.config.html_use_opensearch and self.search: self.info(" opensearch", nonl=1) fn = path.join(self.outdir, "_static", "opensearch.xml") self.handle_page("opensearch", {}, "opensearch.xml", outfilename=fn) self.info() # def write_genindex(self): # # the total count of lines for each index letter, used to distribute # # the entries into two columns # genindex = self.env.create_index(self) # indexcounts = [] # for _k, entries in genindex: # indexcounts.append(sum(1 + len(subitems) # for _, (_, subitems, _) in entries)) # genindexcontext = dict( # genindexentries = genindex, # genindexcounts = indexcounts, # split_index = self.config.html_split_index, # ) # self.info(' genindex', nonl=1) # if self.config.html_split_index: # self.handle_page('genindex', genindexcontext, # 'genindex-split.html') # self.handle_page('genindex-all', genindexcontext, # 'genindex.html') # for (key, entries), count in zip(genindex, indexcounts): # ctx = {'key': key, 'entries': entries, 'count': count, # 'genindexentries': genindex} # self.handle_page('genindex-' + key, ctx, # 'genindex-single.html') # else: # self.handle_page('genindex', genindexcontext, 'genindex.html') def write_domain_indices(self): for indexname, indexcls, content, collapse in self.domain_indices: indexcontext = dict(indextitle=indexcls.localname, content=content, collapse_index=collapse) self.info(" " + indexname, nonl=1) self.handle_page(indexname, indexcontext, "domainindex.html") def copy_image_files(self): # copy image files if self.images: ensuredir(path.join(self.outdir, self.imagedir)) for src in self.app.status_iterator(self.images, "copying images... ", brown, len(self.images)): dest = self.images[src] try: copyfile(path.join(self.srcdir, src), path.join(self.outdir, self.imagedir, dest)) except Exception as err: self.warn("cannot copy image file %r: %s" % (path.join(self.srcdir, src), err)) def copy_download_files(self): def to_relpath(f): return relative_path(self.srcdir, f) # copy downloadable files if self.env.dlfiles: ensuredir(path.join(self.outdir, "_downloads")) for src in self.app.status_iterator( self.env.dlfiles, "copying downloadable files... ", brown, len(self.env.dlfiles), stringify_func=to_relpath, ): dest = self.env.dlfiles[src][1] try: copyfile(path.join(self.srcdir, src), path.join(self.outdir, "_downloads", dest)) except Exception as err: self.warn("cannot copy downloadable file %r: %s" % (path.join(self.srcdir, src), err)) def copy_static_files(self): # copy static files self.info(bold("copying static files... "), nonl=True) ensuredir(path.join(self.outdir, "_static")) ctx = self.globalcontext.copy() # copy over all user-supplied static files staticentries = [path.join(self.confdir, spath) for spath in self.config.html_static_path] matchers = compile_matchers(self.config.exclude_patterns) for entry in staticentries: if not path.exists(entry): self.warn("html_static_path entry %r does not exist" % entry) continue copy_static_entry(entry, path.join(self.outdir, "_static"), self, ctx, exclude_matchers=matchers) self.info("done") def copy_extra_files(self): # copy html_extra_path files self.info(bold("copying extra files... "), nonl=True) extraentries = [path.join(self.confdir, epath) for epath in self.config.html_extra_path] matchers = compile_matchers(self.config.exclude_patterns) for entry in extraentries: if not path.exists(entry): self.warn("html_extra_path entry %r does not exist" % entry) continue copy_extra_entry(entry, self.outdir, matchers) self.info("done") def write_buildinfo(self): # write build info file fp = open(path.join(self.outdir, ".buildinfo"), "w") try: fp.write( "# Sphinx build info version 1\n" "# This file hashes the configuration used when building" " these files. When it is not found, a full rebuild will" " be done.\nconfig: %s\ntags: %s\n" % (self.config_hash, self.tags_hash) ) finally: fp.close() def cleanup(self): pass def post_process_images(self, doctree): """Pick the best candidate for an image and link down-scaled images to their high res version. """ Builder.post_process_images(self, doctree) if self.config.html_scaled_image_link: for node in doctree.traverse(nodes.image): scale_keys = ("scale", "width", "height") if not any((key in node) for key in scale_keys) or isinstance(node.parent, nodes.reference): # docutils does unfortunately not preserve the # ``target`` attribute on images, so we need to check # the parent node here. continue uri = node["uri"] reference = nodes.reference("", "", internal=True) if uri in self.images: reference["refuri"] = posixpath.join(self.imgpath, self.images[uri]) else: reference["refuri"] = uri node.replace_self(reference) reference.append(node) def load_indexer(self, docnames): keep = set(self.env.all_docs) - set(docnames) try: searchindexfn = path.join(self.outdir, self.searchindex_filename) if self.indexer_dumps_unicode: f = codecs.open(searchindexfn, "r", encoding="utf-8") else: f = open(searchindexfn, "rb") try: self.indexer.load(f, self.indexer_format) finally: f.close() except (IOError, OSError, ValueError): if keep: self.warn( "search index couldn't be loaded, but not all " "documents will be built: the index will be " "incomplete." ) # delete all entries for files that will be rebuilt self.indexer.prune(keep) def index_page(self, pagename, doctree, title): # only index pages with title if self.indexer is not None and title: self.indexer.feed(pagename, title, doctree) def _get_local_toctree(self, docname, collapse=True, **kwds): if "includehidden" not in kwds: kwds["includehidden"] = False return self.render_partial(self.env.get_toctree_for(docname, self, collapse, **kwds))["fragment"] # --------- these are overwritten by the serialization builder def handle_page(self, pagename, addctx, templatename="page.html", outfilename=None, event_arg=None): ctx = self.globalcontext.copy() # current_page_name is backwards compatibility ctx["pagename"] = ctx["current_page_name"] = pagename default_baseuri = self.get_target_uri(pagename) # in the singlehtml builder, default_baseuri still contains an #anchor # part, which relative_uri doesn't really like... default_baseuri = default_baseuri.rsplit("#", 1)[0] def pathto(otheruri, resource=False, baseuri=default_baseuri): if resource and "://" in otheruri: # allow non-local resources given by scheme return otheruri elif not resource: otheruri = self.get_target_uri(otheruri) uri = relative_uri(baseuri, otheruri) or "#" return uri ctx["pathto"] = pathto ctx["hasdoc"] = lambda name: name in self.env.all_docs if self.name != "mdhelp": ctx["encoding"] = encoding = self.config.html_output_encoding else: ctx["encoding"] = encoding = self.encoding ctx["toctree"] = lambda **kw: self._get_local_toctree(pagename, **kw) ctx.update(addctx) newtmpl = self.app.emit_firstresult("md-page-context", pagename, templatename, ctx, event_arg) if newtmpl: templatename = newtmpl try: output = self.templates.render(templatename, ctx) except UnicodeError: self.warn( "a Unicode error occurred when rendering the page %s. " "Please make sure all config values that contain " "non-ASCII content are Unicode strings." % pagename ) return if not outfilename: outfilename = self.get_outfilename(pagename) # outfilename's path is in general different from self.outdir ensuredir(path.dirname(outfilename)) try: f = codecs.open(outfilename, "w", encoding, "xmlcharrefreplace") try: f.write(output) finally: f.close() except (IOError, OSError) as err: self.warn("error writing file %s: %s" % (outfilename, err)) if self.copysource and ctx.get("sourcename"): # copy the source file for the "show source" link source_name = path.join(self.outdir, "_sources", os_path(ctx["sourcename"])) ensuredir(path.dirname(source_name)) copyfile(self.env.doc2path(pagename), source_name) def handle_finish(self): if self.indexer: self.finish_tasks.add_task(self.dump_search_index) self.finish_tasks.add_task(self.dump_inventory) def dump_inventory(self): self.info(bold("dumping object inventory... "), nonl=True) f = open(path.join(self.outdir, INVENTORY_FILENAME), "wb") try: f.write( ( u"# Sphinx inventory version 2\n" u"# Project: %s\n" u"# Version: %s\n" u"# The remainder of this file is compressed using zlib.\n" % (self.config.project, self.config.version) ).encode("utf-8") ) compressor = zlib.compressobj(9) for domainname, domain in sorted(self.env.domains.items()): for name, dispname, type, docname, anchor, prio in sorted(domain.get_objects()): if anchor.endswith(name): # this can shorten the inventory by as much as 25% anchor = anchor[: -len(name)] + "$" uri = self.get_target_uri(docname) if anchor: uri += "#" + anchor if dispname == name: dispname = u"-" f.write( compressor.compress( (u"%s %s:%s %s %s %s\n" % (name, domainname, type, prio, uri, dispname)).encode("utf-8") ) ) f.write(compressor.flush()) finally: f.close() self.info("done") def dump_search_index(self): self.info(bold("dumping search index in %s ... " % self.indexer.label()), nonl=True) self.indexer.prune(self.env.all_docs) searchindexfn = path.join(self.outdir, self.searchindex_filename) # first write to a temporary file, so that if dumping fails, # the existing index won't be overwritten if self.indexer_dumps_unicode: f = codecs.open(searchindexfn + ".tmp", "w", encoding="utf-8") else: f = open(searchindexfn + ".tmp", "wb") try: self.indexer.dump(f, self.indexer_format) finally: f.close() movefile(searchindexfn + ".tmp", searchindexfn) self.info("done")