Пример #1
0
def test_trim_doctest_flags():
    PygmentsBridge.html_formatter = MyFormatter
    try:
        bridge = PygmentsBridge("html", trim_doctest_flags=True)
        ret = bridge.highlight_block(">>> 1+2 # doctest: SKIP\n3\n", "pycon")
        assert ret == ">>> 1+2 \n3\n"
    finally:
        PygmentsBridge.html_formatter = HtmlFormatter
Пример #2
0
def test_trim_doctest_flags():
    PygmentsBridge.html_formatter = MyFormatter
    try:
        bridge = PygmentsBridge('html', trim_doctest_flags=True)
        ret = bridge.highlight_block('>>> 1+2 # doctest: SKIP\n3\n', 'pycon')
        assert ret == '>>> 1+2 \n3\n'
    finally:
        PygmentsBridge.html_formatter = HtmlFormatter
Пример #3
0
def test_set_formatter():
    PygmentsBridge.html_formatter = MyFormatter
    try:
        bridge = PygmentsBridge("html")
        ret = bridge.highlight_block("foo\n", "python")
        assert ret == "foo\n"
    finally:
        PygmentsBridge.html_formatter = HtmlFormatter
Пример #4
0
def test_set_formatter():
    PygmentsBridge.html_formatter = MyFormatter
    try:
        bridge = PygmentsBridge('html')
        ret = bridge.highlight_block('foo\n', 'python')
        assert ret == 'foo\n'
    finally:
        PygmentsBridge.html_formatter = HtmlFormatter
Пример #5
0
def test_set_formatter():
    PygmentsBridge.html_formatter = MyFormatter
    try:
        bridge = PygmentsBridge('html')
        ret = bridge.highlight_block('foo\n', 'python')
        assert ret == 'foo\n'
    finally:
        PygmentsBridge.html_formatter = HtmlFormatter
Пример #6
0
 def __init__(self, builder, *args, **kwds):
     BaseTranslator.__init__(self, *args, **kwds)
     self.highlighter = PygmentsBridge('html',
                                       builder.config.pygments_style)
     self.no_smarty = 0
     self.builder = builder
     self.highlightlang = builder.config.highlight_language
     self.highlightlinenothreshold = sys.maxint
     self.protect_literal_text = 0
Пример #7
0
 def init_highlighter(self) -> 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)
Пример #8
0
def _builder_inited(app: sphinx.application.Sphinx) -> None:
    if app.config.html_theme != "furo":
        return

    builder = app.builder
    assert builder.dark_highlighter is None, "this shouldn't happen."
    builder.dark_highlighter = PygmentsBridge("html", app.config.pygments_dark_style)
Пример #9
0
 def __init__(self, builder, *args, **kwds):
     BaseTranslator.__init__(self, *args, **kwds)
     self.highlighter = PygmentsBridge('html', builder.config.pygments_style)
     self.no_smarty = 0
     self.builder = builder
     self.highlightlang = 'python'
     self.highlightlinenothreshold = sys.maxint
Пример #10
0
 def __init__(self, builder, *args, **kwds):
     BaseTranslator.__init__(self, *args, **kwds)
     self.highlighter = PygmentsBridge('html', builder.config.pygments_style)
     self.no_smarty = 0
     self.builder = builder
     self.highlightlang = builder.config.highlight_language
     self.highlightlinenothreshold = sys.maxint
     self.protect_literal_text = 0
Пример #11
0
 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)
Пример #12
0
def test_default_highlight(logger):
    bridge = PygmentsBridge('html')

    # default: highlights as python3
    ret = bridge.highlight_block('print "Hello sphinx world"', 'default')
    assert ret == (
        '<div class="highlight"><pre><span></span><span class="nb">print</span> '
        '<span class="s2">&quot;Hello sphinx world&quot;</span>\n</pre></div>\n'
    )

    # default: fallbacks to none if highlighting failed
    ret = bridge.highlight_block('reST ``like`` text', 'default')
    assert ret == '<div class="highlight"><pre><span></span>reST ``like`` text\n</pre></div>\n'

    # python3: highlights as python3
    ret = bridge.highlight_block('print "Hello sphinx world"', 'python3')
    assert ret == (
        '<div class="highlight"><pre><span></span><span class="nb">print</span> '
        '<span class="s2">&quot;Hello sphinx world&quot;</span>\n</pre></div>\n'
    )

    # python3: raises error if highlighting failed
    ret = bridge.highlight_block('reST ``like`` text', 'python3')
    logger.warning.assert_called_with(
        'Could not lex literal_block as "%s". '
        'Highlighting skipped.',
        'python3',
        type='misc',
        subtype='highlighting_failure',
        location=None)
Пример #13
0
def test_default_highlight():
    bridge = PygmentsBridge('html')

    # default: highlights as python3
    ret = bridge.highlight_block('print "Hello sphinx world"', 'default')
    assert ret == (
        '<div class="highlight"><pre><span></span><span class="nb">print</span> '
        '<span class="s2">&quot;Hello sphinx world&quot;</span>\n</pre></div>\n'
    )

    # default: fallbacks to none if highlighting failed
    ret = bridge.highlight_block('reST ``like`` text', 'default')
    assert ret == '<div class="highlight"><pre><span></span>reST ``like`` text\n</pre></div>\n'

    # python3: highlights as python3
    ret = bridge.highlight_block('print "Hello sphinx world"', 'python3')
    assert ret == (
        '<div class="highlight"><pre><span></span><span class="nb">print</span> '
        '<span class="s2">&quot;Hello sphinx world&quot;</span>\n</pre></div>\n'
    )

    # python3: raises error if highlighting failed
    try:
        ret = bridge.highlight_block('reST ``like`` text', 'python3')
        assert False, "highlight_block() does not raise any exceptions"
    except ErrorToken:
        pass  # raise parsing error
Пример #14
0
def test_default_highlight():
    bridge = PygmentsBridge('html')

    # default: highlights as python3
    ret = bridge.highlight_block('print "Hello sphinx world"', 'default')
    assert ret == ('<div class="highlight"><pre><span></span><span class="nb">print</span> '
                   '<span class="s2">&quot;Hello sphinx world&quot;</span>\n</pre></div>\n')

    # default: fallbacks to none if highlighting failed
    ret = bridge.highlight_block('reST ``like`` text', 'default')
    assert ret == '<div class="highlight"><pre><span></span>reST ``like`` text\n</pre></div>\n'

    # python3: highlights as python3
    ret = bridge.highlight_block('print "Hello sphinx world"', 'python3')
    assert ret == ('<div class="highlight"><pre><span></span><span class="nb">print</span> '
                   '<span class="s2">&quot;Hello sphinx world&quot;</span>\n</pre></div>\n')

    # python3: raises error if highlighting failed
    try:
        ret = bridge.highlight_block('reST ``like`` text', 'python3')
        assert False, "highlight_block() does not raise any exceptions"
    except ErrorToken:
        pass  # raise parsing error
Пример #15
0
def test_default_highlight(logger):
    bridge = PygmentsBridge('html')

    # default: highlights as python3
    ret = bridge.highlight_block('print "Hello sphinx world"', 'default')
    assert ret == ('<div class="highlight"><pre><span></span><span class="nb">print</span> '
                   '<span class="s2">&quot;Hello sphinx world&quot;</span>\n</pre></div>\n')

    # default: fallbacks to none if highlighting failed
    ret = bridge.highlight_block('reST ``like`` text', 'default')
    assert ret == '<div class="highlight"><pre><span></span>reST ``like`` text\n</pre></div>\n'

    # python3: highlights as python3
    ret = bridge.highlight_block('print "Hello sphinx world"', 'python3')
    assert ret == ('<div class="highlight"><pre><span></span><span class="nb">print</span> '
                   '<span class="s2">&quot;Hello sphinx world&quot;</span>\n</pre></div>\n')

    # python3: raises error if highlighting failed
    ret = bridge.highlight_block('reST ``like`` text', 'python3')
    logger.warning.assert_called_with('Could not lex literal_block as "%s". '
                                      'Highlighting skipped.', 'python3',
                                      type='misc', subtype='highlighting_failure',
                                      location=None)
Пример #16
0
 def init(self):
     # writer object is initialized in prepare_writing method
     self.writer = None
     # section numbers for headings in the currently visited document
     self.secnumbers = {}
     # figure numbers
     self.fignumbers = {}
     # currently written docname
     self.current_docname = None  # type: unicode
     # sphinx highlighter, from StandaloneHTMLBuilder.init_highlighter()
     self.highlighter = PygmentsBridge(
         'html',
         'sphinx',
         self.config.trim_doctest_flags)
Пример #17
0
def _builder_inited(app: sphinx.application.Sphinx) -> None:
    if app.config.html_theme != "furo":
        return

    builders = [
        "html",
        "singlehtml",
        "dirhtml",
        "readthedocs",
        "readthedocsdirhtml",
        "readthedocssinglehtml",
        "readthedocssinglehtmllocalmedia",
        "spelling",
    ]

    if not app.builder.format in builders:
        return

    builder = app.builder
    assert builder.dark_highlighter is None, "this shouldn't happen."
    builder.dark_highlighter = PygmentsBridge("html", app.config.pygments_dark_style)
Пример #18
0
def make_lexer(lang: str = None,
               code: str = None,
               language_lexer: Lexer = None):
    """Return a subclass of HighlightedCodeLexer and the language lexer class which is
       either deduced from lang (str) or code (str), or the class of language_lexer
       (pygments.lexer) instance.

    Params:
        language_lexer (pygments.Lexer): an instance of the lexer class to subclass
        lang (str): name of the language
        code (str): code to be lexed
    """

    if not (lang or code or language_lexer):
        raise ArgumentError(
            "make_lexer() requires one of lang(str) and code(str) "
            "or language_lexer(pygments.Lexer) keyword arguments")

    # let sphinx figure out the language based on the content and/or language name
    if not language_lexer:
        language_lexer = PygmentsBridge().get_lexer(source=code,
                                                    lang=lang or "guess")

    # get the lexer class to inherit from
    parent = language_lexer.__class__

    # dynamically define metaclass for __repr__
    meta = type(f"{parent.__name__.replace('Lexer', '')}Meta",
                (type(parent), ), {"__repr__": __meta_repr__})

    # dynamically define lexer class
    klass = new_class(
        f"Highlighted{parent.__name__}",
        (HighlightedCodeLexer, parent),
        {"metaclass": meta},
    )
    klass.parent = parent

    return klass
Пример #19
0
def test_add_lexer(app, status, warning):
    app.add_lexer('test', MyLexer())

    bridge = PygmentsBridge('html')
    ret = bridge.highlight_block('ab', 'test')
    assert '<span class="n">a</span>b' in ret
Пример #20
0
 def init_highlighter(self):
     self.highlighter = PygmentsBridge('html', 'sphinx',
                                       self.config.trim_doctest_flags)
Пример #21
0
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')
Пример #22
0
class HTMLTranslator(BaseTranslator):
    """
    Our custom HTML translator.
    """
    def __init__(self, builder, *args, **kwds):
        BaseTranslator.__init__(self, *args, **kwds)
        self.highlighter = PygmentsBridge('html',
                                          builder.config.pygments_style)
        self.no_smarty = 0
        self.builder = builder
        self.highlightlang = builder.config.highlight_language
        self.highlightlinenothreshold = sys.maxint
        self.protect_literal_text = 0

    def visit_desc(self, node):
        self.body.append(self.starttag(node, 'dl', CLASS=node['desctype']))

    def depart_desc(self, node):
        self.body.append('</dl>\n\n')

    def visit_desc_signature(self, node):
        # the id is set automatically
        self.body.append(self.starttag(node, 'dt'))
        # anchor for per-desc interactive data
        if node.parent['desctype'] != 'describe' and node['ids'] and node[
                'first']:
            self.body.append('<!--[%s]-->' % node['ids'][0])
        if node.parent['desctype'] in ('class', 'exception'):
            self.body.append('%s ' % node.parent['desctype'])

    def depart_desc_signature(self, node):
        if node['ids'] and self.builder.add_definition_links:
            self.body.append(
                u'<a class="headerlink" href="#%s" ' % node['ids'][0] +
                u'title="%s">\u00B6</a>' % _('Permalink to this definition'))
        self.body.append('</dt>\n')

    def visit_desc_addname(self, node):
        self.body.append(self.starttag(node, 'tt', '', CLASS='descclassname'))

    def depart_desc_addname(self, node):
        self.body.append('</tt>')

    def visit_desc_type(self, node):
        pass

    def depart_desc_type(self, node):
        pass

    def visit_desc_name(self, node):
        self.body.append(self.starttag(node, 'tt', '', CLASS='descname'))

    def depart_desc_name(self, node):
        self.body.append('</tt>')

    def visit_desc_parameterlist(self, node):
        self.body.append('<big>(</big>')
        self.first_param = 1

    def depart_desc_parameterlist(self, node):
        self.body.append('<big>)</big>')

    def visit_desc_parameter(self, node):
        if not self.first_param:
            self.body.append(', ')
        else:
            self.first_param = 0
        if not node.hasattr('noemph'):
            self.body.append('<em>')

    def depart_desc_parameter(self, node):
        if not node.hasattr('noemph'):
            self.body.append('</em>')

    def visit_desc_optional(self, node):
        self.body.append('<span class="optional">[</span>')

    def depart_desc_optional(self, node):
        self.body.append('<span class="optional">]</span>')

    def visit_desc_annotation(self, node):
        self.body.append(self.starttag(node, 'em', CLASS='property'))

    def depart_desc_annotation(self, node):
        self.body.append('</em>')

    def visit_desc_content(self, node):
        self.body.append(self.starttag(node, 'dd', ''))

    def depart_desc_content(self, node):
        self.body.append('</dd>')

    def visit_refcount(self, node):
        self.body.append(self.starttag(node, 'em', '', CLASS='refcount'))

    def depart_refcount(self, node):
        self.body.append('</em>')

    def visit_versionmodified(self, node):
        self.body.append(self.starttag(node, 'p'))
        text = versionlabels[node['type']] % node['version']
        if len(node):
            text += ': '
        else:
            text += '.'
        self.body.append('<span class="versionmodified">%s</span>' % text)

    def depart_versionmodified(self, node):
        self.body.append('</p>\n')

    # overwritten
    def visit_reference(self, node):
        BaseTranslator.visit_reference(self, node)
        if node.hasattr('reftitle'):
            # ugly hack to add a title attribute
            starttag = self.body[-1]
            if not starttag.startswith('<a '):
                return
            self.body[-1] = '<a title="%s"' % self.attval(node['reftitle']) + \
                            starttag[2:]

    # overwritten -- we don't want source comments to show up in the HTML
    def visit_comment(self, node):
        raise nodes.SkipNode

    # overwritten
    def visit_admonition(self, node, name=''):
        self.body.append(
            self.starttag(node, 'div', CLASS=('admonition ' + name)))
        if name and name != 'seealso':
            node.insert(0, nodes.title(name, admonitionlabels[name]))
        self.set_first_last(node)

    def visit_seealso(self, node):
        self.visit_admonition(node, 'seealso')

    def depart_seealso(self, node):
        self.depart_admonition(node)

    # overwritten for docutils 0.4
    if hasattr(BaseTranslator, 'start_tag_with_title'):

        def visit_section(self, node):
            # the 0.5 version, to get the id attribute in the <div> tag
            self.section_level += 1
            self.body.append(self.starttag(node, 'div', CLASS='section'))

        def visit_title(self, node):
            # don't move the id attribute inside the <h> tag
            BaseTranslator.visit_title(self, node, move_ids=0)

    # overwritten
    def visit_literal_block(self, node):
        if node.rawsource != node.astext():
            # most probably a parsed-literal block -- don't highlight
            return BaseTranslator.visit_literal_block(self, node)
        lang = self.highlightlang
        linenos = node.rawsource.count(
            '\n') >= self.highlightlinenothreshold - 1
        if node.has_key('language'):
            # code-block directives
            lang = node['language']
        if node.has_key('linenos'):
            linenos = node['linenos']
        self.body.append(
            self.highlighter.highlight_block(node.rawsource, lang, linenos))
        raise nodes.SkipNode

    def visit_doctest_block(self, node):
        self.visit_literal_block(node)

    # overwritten
    def visit_literal(self, node):
        if len(node.children) == 1 and \
               node.children[0] in ('None', 'True', 'False'):
            node['classes'].append('xref')
        self.body.append(
            self.starttag(node, 'tt', '', CLASS='docutils literal'))
        self.protect_literal_text += 1

    def depart_literal(self, node):
        self.protect_literal_text -= 1
        self.body.append('</tt>')

    def visit_productionlist(self, node):
        self.body.append(self.starttag(node, 'pre'))
        names = []
        for production in node:
            names.append(production['tokenname'])
        maxlen = max(len(name) for name in names)
        for production in node:
            if production['tokenname']:
                lastname = production['tokenname'].ljust(maxlen)
                self.body.append(self.starttag(production, 'strong', ''))
                self.body.append(lastname + '</strong> ::= ')
            else:
                self.body.append('%s     ' % (' ' * len(lastname)))
            production.walkabout(self)
            self.body.append('\n')
        self.body.append('</pre>\n')
        raise nodes.SkipNode

    def depart_productionlist(self, node):
        pass

    def visit_production(self, node):
        pass

    def depart_production(self, node):
        pass

    def visit_centered(self, node):
        self.body.append(
            self.starttag(node, 'p', CLASS="centered") + '<strong>')

    def depart_centered(self, node):
        self.body.append('</strong></p>')

    def visit_compact_paragraph(self, node):
        pass

    def depart_compact_paragraph(self, node):
        pass

    def visit_highlightlang(self, node):
        self.highlightlang = node['lang']
        self.highlightlinenothreshold = node['linenothreshold']

    def depart_highlightlang(self, node):
        pass

    # overwritten
    def visit_image(self, node):
        olduri = node['uri']
        # rewrite the URI if the environment knows about it
        if olduri in self.builder.images:
            node['uri'] = posixpath.join(self.builder.imgpath,
                                         self.builder.images[olduri])
        BaseTranslator.visit_image(self, node)

    def visit_toctree(self, node):
        # this only happens when formatting a toc from env.tocs -- in this
        # case we don't want to include the subtree
        raise nodes.SkipNode

    def visit_index(self, node):
        raise nodes.SkipNode

    def visit_tabular_col_spec(self, node):
        raise nodes.SkipNode

    def visit_glossary(self, node):
        pass

    def depart_glossary(self, node):
        pass

    def visit_acks(self, node):
        pass

    def depart_acks(self, node):
        pass

    def visit_module(self, node):
        pass

    def depart_module(self, node):
        pass

    def bulk_text_processor(self, text):
        return text

    # overwritten
    def visit_Text(self, node):
        text = node.astext()
        encoded = self.encode(text)
        if self.protect_literal_text:
            # moved here from base class's visit_literal to support
            # more formatting in literal nodes
            for token in self.words_and_spaces.findall(encoded):
                if token.strip():
                    # protect literal text from line wrapping
                    self.body.append('<span class="pre">%s</span>' % token)
                elif token in ' \n':
                    # allow breaks at whitespace
                    self.body.append(token)
                else:
                    # protect runs of multiple spaces; the last one can wrap
                    self.body.append('&nbsp;' * (len(token) - 1) + ' ')
        else:
            if self.in_mailto and self.settings.cloak_email_addresses:
                encoded = self.cloak_email(encoded)
            else:
                encoded = self.bulk_text_processor(encoded)
            self.body.append(encoded)

    # these are all for docutils 0.5 compatibility

    def visit_note(self, node):
        self.visit_admonition(node, 'note')

    def depart_note(self, node):
        self.depart_admonition(node)

    def visit_warning(self, node):
        self.visit_admonition(node, 'warning')

    def depart_warning(self, node):
        self.depart_admonition(node)

    def visit_attention(self, node):
        self.visit_admonition(node, 'attention')

    def depart_attention(self, node):
        self.depart_admonition()

    def visit_caution(self, node):
        self.visit_admonition(node, 'caution')

    def depart_caution(self, node):
        self.depart_admonition()

    def visit_danger(self, node):
        self.visit_admonition(node, 'danger')

    def depart_danger(self, node):
        self.depart_admonition()

    def visit_error(self, node):
        self.visit_admonition(node, 'error')

    def depart_error(self, node):
        self.depart_admonition()

    def visit_hint(self, node):
        self.visit_admonition(node, 'hint')

    def depart_hint(self, node):
        self.depart_admonition()

    def visit_important(self, node):
        self.visit_admonition(node, 'important')

    def depart_important(self, node):
        self.depart_admonition()

    def visit_tip(self, node):
        self.visit_admonition(node, 'tip')

    def depart_tip(self, node):
        self.depart_admonition()

    # these are only handled specially in the SmartyPantsHTMLTranslator
    def visit_literal_emphasis(self, node):
        return self.visit_emphasis(node)

    def depart_literal_emphasis(self, node):
        return self.depart_emphasis(node)

    def depart_title(self, node):
        close_tag = self.context[-1]
        if self.builder.add_header_links and \
               (close_tag.startswith('</h') or
                close_tag.startswith('</a></h')) and \
               node.parent.hasattr('ids') and node.parent['ids']:
            aname = node.parent['ids'][0]
            # add permalink anchor
            self.body.append(u'<a class="headerlink" href="#%s" ' % aname +
                             u'title="%s">\u00B6</a>' %
                             _('Permalink to this headline'))
        BaseTranslator.depart_title(self, node)

    def unknown_visit(self, node):
        raise NotImplementedError('Unknown node: ' + node.__class__.__name__)
Пример #23
0
class HTMLTranslator(BaseTranslator):
    """
    Our custom HTML translator.
    """

    def __init__(self, builder, *args, **kwds):
        BaseTranslator.__init__(self, *args, **kwds)
        self.highlighter = PygmentsBridge('html', builder.config.pygments_style)
        self.no_smarty = 0
        self.builder = builder
        self.highlightlang = 'python'
        self.highlightlinenothreshold = sys.maxint
        self.protect_literal_text = 0

    def visit_desc(self, node):
        self.body.append(self.starttag(node, 'dl', CLASS=node['desctype']))
    def depart_desc(self, node):
        self.body.append('</dl>\n\n')

    def visit_desc_signature(self, node):
        # the id is set automatically
        self.body.append(self.starttag(node, 'dt'))
        # anchor for per-desc interactive data
        if node.parent['desctype'] != 'describe' and node['ids'] and node['first']:
            self.body.append('<!--[%s]-->' % node['ids'][0])
        if node.parent['desctype'] in ('class', 'exception'):
            self.body.append('%s ' % node.parent['desctype'])
    def depart_desc_signature(self, node):
        if node['ids'] and self.builder.name != 'htmlhelp':
            self.body.append(u'<a class="headerlink" href="#%s" ' % node['ids'][0] +
                             u'title="Permalink to this definition">\u00B6</a>')
        self.body.append('</dt>\n')

    def visit_desc_addname(self, node):
        self.body.append(self.starttag(node, 'tt', '', CLASS='descclassname'))
    def depart_desc_addname(self, node):
        self.body.append('</tt>')

    def visit_desc_type(self, node):
        pass
    def depart_desc_type(self, node):
        pass

    def visit_desc_name(self, node):
        self.body.append(self.starttag(node, 'tt', '', CLASS='descname'))
    def depart_desc_name(self, node):
        self.body.append('</tt>')

    def visit_desc_parameterlist(self, node):
        self.body.append('<big>(</big>')
        self.first_param = 1
    def depart_desc_parameterlist(self, node):
        self.body.append('<big>)</big>')

    def visit_desc_parameter(self, node):
        if not self.first_param:
            self.body.append(', ')
        else:
            self.first_param = 0
        if not node.hasattr('noemph'):
            self.body.append('<em>')
    def depart_desc_parameter(self, node):
        if not node.hasattr('noemph'):
            self.body.append('</em>')

    def visit_desc_optional(self, node):
        self.body.append('<span class="optional">[</span>')
    def depart_desc_optional(self, node):
        self.body.append('<span class="optional">]</span>')

    def visit_desc_annotation(self, node):
        self.body.append(self.starttag(node, 'em', CLASS='property'))
    def depart_desc_annotation(self, node):
        self.body.append('</em>')

    def visit_desc_content(self, node):
        self.body.append(self.starttag(node, 'dd', ''))
    def depart_desc_content(self, node):
        self.body.append('</dd>')

    def visit_refcount(self, node):
        self.body.append(self.starttag(node, 'em', '', CLASS='refcount'))
    def depart_refcount(self, node):
        self.body.append('</em>')

    def visit_versionmodified(self, node):
        self.body.append(self.starttag(node, 'p'))
        text = version_text[node['type']] % node['version']
        if len(node):
            text += ': '
        else:
            text += '.'
        self.body.append('<span class="versionmodified">%s</span>' % text)
    def depart_versionmodified(self, node):
        self.body.append('</p>\n')

    # overwritten
    def visit_reference(self, node):
        BaseTranslator.visit_reference(self, node)
        if node.hasattr('reftitle'):
            # ugly hack to add a title attribute
            starttag = self.body[-1]
            if not starttag.startswith('<a '):
                return
            self.body[-1] = '<a title="%s"' % self.attval(node['reftitle']) + \
                            starttag[2:]

    # overwritten -- we don't want source comments to show up in the HTML
    def visit_comment(self, node):
        raise nodes.SkipNode

    # overwritten
    def visit_admonition(self, node, name=''):
        self.body.append(self.starttag(
            node, 'div', CLASS=('admonition ' + name)))
        if name and name != 'seealso':
            node.insert(0, nodes.title(name, self.language.labels[name]))
        self.set_first_last(node)

    def visit_seealso(self, node):
        self.visit_admonition(node, 'seealso')
    def depart_seealso(self, node):
        self.depart_admonition(node)

    # overwritten for docutils 0.4
    if hasattr(BaseTranslator, 'start_tag_with_title'):
        def visit_section(self, node):
            # the 0.5 version, to get the id attribute in the <div> tag
            self.section_level += 1
            self.body.append(self.starttag(node, 'div', CLASS='section'))

        def visit_title(self, node):
            # don't move the id attribute inside the <h> tag
            BaseTranslator.visit_title(self, node, move_ids=0)

    # overwritten
    def visit_literal_block(self, node):
        if node.rawsource != node.astext():
            # most probably a parsed-literal block -- don't highlight
            return BaseTranslator.visit_literal_block(self, node)
        lang = self.highlightlang
        linenos = node.rawsource.count('\n') >= self.highlightlinenothreshold - 1
        if node.has_key('language'):
            # code-block directives
            lang = node['language']
        if node.has_key('linenos'):
            linenos = node['linenos']
        self.body.append(self.highlighter.highlight_block(node.rawsource,
                                                          lang, linenos))
        raise nodes.SkipNode

    def visit_doctest_block(self, node):
        self.visit_literal_block(node)

    # overwritten
    def visit_literal(self, node):
        if len(node.children) == 1 and \
               node.children[0] in ('None', 'True', 'False'):
            node['classes'].append('xref')
        self.body.append(self.starttag(node, 'tt', '', CLASS='docutils literal'))
        self.protect_literal_text += 1
    def depart_literal(self, node):
        self.protect_literal_text -= 1
        self.body.append('</tt>')

    def visit_productionlist(self, node):
        self.body.append(self.starttag(node, 'pre'))
        names = []
        for production in node:
            names.append(production['tokenname'])
        maxlen = max(len(name) for name in names)
        for production in node:
            if production['tokenname']:
                lastname = production['tokenname'].ljust(maxlen)
                self.body.append(self.starttag(production, 'strong', ''))
                self.body.append(lastname + '</strong> ::= ')
            else:
                self.body.append('%s     ' % (' '*len(lastname)))
            production.walkabout(self)
            self.body.append('\n')
        self.body.append('</pre>\n')
        raise nodes.SkipNode
    def depart_productionlist(self, node):
        pass

    def visit_production(self, node):
        pass
    def depart_production(self, node):
        pass

    def visit_centered(self, node):
        self.body.append(self.starttag(node, 'p', CLASS="centered") + '<strong>')
    def depart_centered(self, node):
        self.body.append('</strong></p>')

    def visit_compact_paragraph(self, node):
        pass
    def depart_compact_paragraph(self, node):
        pass

    def visit_highlightlang(self, node):
        self.highlightlang = node['lang']
        self.highlightlinenothreshold = node['linenothreshold']
    def depart_highlightlang(self, node):
        pass

    # overwritten
    def visit_image(self, node):
        olduri = node['uri']
        # rewrite the URI if the environment knows about it
        if olduri in self.builder.images:
            node['uri'] = posixpath.join(self.builder.imgpath,
                                         self.builder.images[olduri])
        BaseTranslator.visit_image(self, node)

    def visit_toctree(self, node):
        # this only happens when formatting a toc from env.tocs -- in this
        # case we don't want to include the subtree
        raise nodes.SkipNode

    def visit_index(self, node):
        raise nodes.SkipNode

    def visit_tabular_col_spec(self, node):
        raise nodes.SkipNode

    def visit_glossary(self, node):
        pass
    def depart_glossary(self, node):
        pass

    def visit_acks(self, node):
        pass
    def depart_acks(self, node):
        pass

    def visit_module(self, node):
        pass
    def depart_module(self, node):
        pass

    def bulk_text_processor(self, text):
        return text

    # overwritten
    def visit_Text(self, node):
        text = node.astext()
        encoded = self.encode(text)
        if self.protect_literal_text:
            # moved here from base class's visit_literal to support
            # more formatting in literal nodes
            for token in self.words_and_spaces.findall(encoded):
                if token.strip():
                    # protect literal text from line wrapping
                    self.body.append('<span class="pre">%s</span>' % token)
                elif token in ' \n':
                    # allow breaks at whitespace
                    self.body.append(token)
                else:
                    # protect runs of multiple spaces; the last one can wrap
                    self.body.append('&nbsp;' * (len(token)-1) + ' ')
        else:
            if self.in_mailto and self.settings.cloak_email_addresses:
                encoded = self.cloak_email(encoded)
            else:
                encoded = self.bulk_text_processor(encoded)
            self.body.append(encoded)

    # these are all for docutils 0.5 compatibility

    def visit_note(self, node):
        self.visit_admonition(node, 'note')
    def depart_note(self, node):
        self.depart_admonition(node)

    def visit_warning(self, node):
        self.visit_admonition(node, 'warning')
    def depart_warning(self, node):
        self.depart_admonition(node)

    def visit_attention(self, node):
        self.visit_admonition(node, 'attention')

    def depart_attention(self, node):
        self.depart_admonition()

    def visit_caution(self, node):
        self.visit_admonition(node, 'caution')
    def depart_caution(self, node):
        self.depart_admonition()

    def visit_danger(self, node):
        self.visit_admonition(node, 'danger')
    def depart_danger(self, node):
        self.depart_admonition()

    def visit_error(self, node):
        self.visit_admonition(node, 'error')
    def depart_error(self, node):
        self.depart_admonition()

    def visit_hint(self, node):
        self.visit_admonition(node, 'hint')
    def depart_hint(self, node):
        self.depart_admonition()

    def visit_important(self, node):
        self.visit_admonition(node, 'important')
    def depart_important(self, node):
        self.depart_admonition()

    def visit_tip(self, node):
        self.visit_admonition(node, 'tip')
    def depart_tip(self, node):
        self.depart_admonition()

    # these are only handled specially in the SmartyPantsHTMLTranslator
    def visit_literal_emphasis(self, node):
        return self.visit_emphasis(node)
    def depart_literal_emphasis(self, node):
        return self.depart_emphasis(node)

    def depart_title(self, node):
        close_tag = self.context[-1]
        if self.builder.name != 'htmlhelp' and \
               (close_tag.startswith('</h') or
                close_tag.startswith('</a></h')) and \
               node.parent.hasattr('ids') and node.parent['ids']:
            aname = node.parent['ids'][0]
            # add permalink anchor
            self.body.append(u'<a class="headerlink" href="#%s" ' % aname +
                             u'title="Permalink to this headline">\u00B6</a>')
        BaseTranslator.depart_title(self, node)

    def unknown_visit(self, node):
        raise NotImplementedError("Unknown node: " + node.__class__.__name__)
Пример #24
0
def _builder_inited(app: sphinx.application.Sphinx) -> None:
    if app.config.html_theme != "furo":
        return

    # Our `main.js` file needs to be loaded as soon as possible.
    app.add_js_file("scripts/main.js", priority=200)

    # 500 is the default priority for extensions, we want this after this.
    app.add_css_file("styles/furo-extensions.css", priority=600)

    builder = app.builder
    assert builder.dark_highlighter is None, "this shouldn't happen."

    # number_of_hours_spent_figuring_this_out = 7
    #
    # Hello human in the future! This next block of code needs a bit of a story, and
    # if you're going to touch it, remember to update the number above (or remove this
    # comment entirely).
    #
    # Hopefully, you know that Sphinx allows extensions and themes to add configuration
    # values via `app.add_config_value`. This usually lets users set those values from
    # `conf.py` while allowing the extension to read from it and utilise that information.
    # As any reasonable person who's written a Sphinx extension before, you would
    # expect the following to work:
    #
    #     dark_style = app.config.pygments_dark_style
    #
    # Turns out, no. How dare you expect things to just work!? That stuff just returns
    # the default value provided when calling `app.add_config_value`. Yes, even if you
    # set it in `conf.py`. Why? Good question. :)
    #
    # The logic in Sphinx literally looks it up in the same mapping as what was
    # manipulated by `add_config_value`, and there's no other spot where that value
    # gets manipulated. I spent a bunch of time debugging how that class works, and...
    # yea, I can't figure it out. There's multiple mappings floating around and bunch
    # of manipulation being done for all kinds of things.
    #
    # The only place on the config object where I was able to find the user-provided
    # value from `conf.py` is a private variable `self._raw_config`. Those values are
    # supposed to get added to self.__dict__[...], and generally be accessible through
    # the object's custom `__getattr__`.
    #
    # Anyway, after giving up on figuring out how to file a PR to fix this upstream, I
    # started looking for hacky ways to get this without reaching into private
    # variables. That quest led to a very simple conclusion: no, you can't do that.
    #
    # So, here we are: with the only option being to reach into the guts of the beast,
    # and pull out the specific thing that's needed. This is obviously fragile though,
    # so this is written with the assumption that any changes to Sphinx's config
    # object's internals would correspond to the originally expected behaviour working.
    # This is so that when any of Sphinx's internals change, this logic would basically
    # fall back to the original behaviour and also print a warning, so that hopefully
    # someone will report this. Maybe it'll all be fixed, and I can remove this whole
    # hack and this giant comment.
    #
    # But wait, this hack actually has another layer to it.
    #
    # This whole setup depends on an internal implementation detail in Sphinx -- that
    # it "adds" the `pygments_dark.css` file for inclusion in output, at a different
    # point than where it is generates the file. The dark syntax highlighting mechanism
    # of this theme depends on that fact -- we don't actually set `pygments_dark_style`
    # in our theme.conf file.
    #
    # Instead, we stick our filthy monkey hands into Sphinx's builder, to patch the
    # builder to generate the `pygments_dark.css` file as if this theme actually sets
    # `pygments_dark_style`. This results in Sphinx generating the file without
    # injecting a custom CSS file for it. Then, we include that stylesheet in our HTML
    # via a hand-crafted <link> tag. There's 2 benefits to this approach: (1) it works,
    # (2) we can, at some point in the future, pivot to a different strategy for
    # including the dark mode syntax highlighting styles.

    # HACK: begins here
    dark_style = None
    try:
        if (hasattr(app.config, "_raw_config")
                and isinstance(app.config._raw_config, dict)
                and "pygments_dark_style" in app.config._raw_config):
            dark_style = app.config._raw_config["pygments_dark_style"]
    except (AttributeError, KeyError) as e:
        logger.warn(
            ("Furo could not determine the value of `pygments_dark_style`. "
             "Falling back to using the value provided by Sphinx.\n"
             "Caused by %s"),
            e,
        )

    if dark_style is None:
        dark_style = app.config.pygments_dark_style

    builder.dark_highlighter = PygmentsBridge("html", dark_style)
Пример #25
0
class HTMLTranslator(BaseTranslator):
    """
    Our custom HTML translator.
    """

    def __init__(self, builder, *args, **kwds):
        BaseTranslator.__init__(self, *args, **kwds)
        self.highlighter = PygmentsBridge('html', builder.config.pygments_style)
        self.no_smarty = 0
        self.builder = builder
        self.highlightlang = 'python'
        self.highlightlinenothreshold = sys.maxint
        self.language.labels['warning'] = 'Caveat'

    def visit_desc(self, node):
        self.body.append(self.starttag(node, 'dl', CLASS=node['desctype']))
    def depart_desc(self, node):
        self.body.append('</dl>\n\n')

    def visit_desc_signature(self, node):
        # the id is set automatically
        self.body.append(self.starttag(node, 'dt'))
        # anchor for per-desc interactive data
        if node.parent['desctype'] != 'describe' and node['ids'] and node['first']:
            self.body.append('<!--#%s#-->' % node['ids'][0])
        if node.parent['desctype'] in ('class', 'exception'):
            self.body.append('%s ' % node.parent['desctype'])
    def depart_desc_signature(self, node):
        if node['ids'] and self.builder.name != 'htmlhelp':
            self.body.append(u'<a class="headerlink" href="#%s" ' % node['ids'][0] +
                             u'title="Permalink to this definition">\u00B6</a>')
        self.body.append('</dt>\n')

    def visit_desc_classname(self, node):
        self.body.append(self.starttag(node, 'tt', '', CLASS='descclassname'))
    def depart_desc_classname(self, node):
        self.body.append('</tt>')

    def visit_desc_type(self, node):
        # return type of C functions -- nothing to do here
        pass
    def depart_desc_type(self, node):
        pass

    def visit_desc_name(self, node):
        self.body.append(self.starttag(node, 'tt', '', CLASS='descname'))
    def depart_desc_name(self, node):
        self.body.append('</tt>')

    def visit_desc_parameterlist(self, node):
        self.body.append('<big>(</big>')
        self.first_param = 1
    def depart_desc_parameterlist(self, node):
        self.body.append('<big>)</big>')

    def visit_desc_parameter(self, node):
        if not self.first_param:
            self.body.append(', ')
        else:
            self.first_param = 0
        if not node.hasattr('noemph'):
            self.body.append('<em>')
    def depart_desc_parameter(self, node):
        if not node.hasattr('noemph'):
            self.body.append('</em>')

    def visit_desc_optional(self, node):
        self.body.append('<span class="optional">[</span>')
    def depart_desc_optional(self, node):
        self.body.append('<span class="optional">]</span>')

    def visit_desc_content(self, node):
        self.body.append(self.starttag(node, 'dd', ''))
    def depart_desc_content(self, node):
        self.body.append('</dd>')

    def visit_refcount(self, node):
        self.body.append(self.starttag(node, 'em', '', CLASS='refcount'))
    def depart_refcount(self, node):
        self.body.append('</em>')

    def visit_versionmodified(self, node):
        self.body.append(self.starttag(node, 'p'))
        text = version_text[node['type']] % node['version']
        if len(node):
            text += ': '
        else:
            text += '.'
        self.body.append('<span class="versionmodified">%s</span>' % text)
    def depart_versionmodified(self, node):
        self.body.append('</p>\n')

    # overwritten
    def visit_reference(self, node):
        BaseTranslator.visit_reference(self, node)
        if node.hasattr('reftitle'):
            # ugly hack to add a title attribute
            starttag = self.body[-1]
            if not starttag.startswith('<a '):
                return
            self.body[-1] = '<a title="%s"' % self.attval(node['reftitle']) + \
                            starttag[2:]

    # overwritten -- we don't want source comments to show up in the HTML
    def visit_comment(self, node):
        raise nodes.SkipNode

    # overwritten
    def visit_admonition(self, node, name=''):
        self.body.append(self.starttag(
            node, 'div', CLASS=('admonition ' + name)))
        if name and name != 'seealso':
            node.insert(0, nodes.title(name, self.language.labels[name]))
        self.set_first_last(node)

    def visit_seealso(self, node):
        self.visit_admonition(node, 'seealso')
    def depart_seealso(self, node):
        self.depart_admonition(node)

    # overwritten (args/kwds due to docutils 0.4/0.5 incompatibility)
    def visit_title(self, node, *args, **kwds):
        # if we have a section we do our own processing in order
        # to have ids in the hN-tags and not in additional a-tags
        if isinstance(node.parent, nodes.section):
            h_level = self.section_level + self.initial_header_level - 1
            if node.parent.get('ids'):
                attrs = {'ids': node.parent['ids']}
            else:
                attrs = {}
            self.body.append(self.starttag(node, 'h%d' % h_level, '', **attrs))
            self.context.append('</h%d>\n' % h_level)
        else:
            BaseTranslator.visit_title(self, node, *args, **kwds)

    # overwritten
    def visit_literal_block(self, node):
        if node.rawsource != node.astext():
            # most probably a parsed-literal block -- don't highlight
            return BaseTranslator.visit_literal_block(self, node)
        lang = self.highlightlang
        linenos = node.rawsource.count('\n') >= self.highlightlinenothreshold - 1
        if node.has_key('language'):
            # code-block directives
            lang = node['language']
        if node.has_key('linenos'):
            linenos = node['linenos']
        self.body.append(self.highlighter.highlight_block(node.rawsource,
                                                          lang, linenos))
        raise nodes.SkipNode

    def visit_doctest_block(self, node):
        self.visit_literal_block(node)

    # overwritten
    def visit_literal(self, node):
        if len(node.children) == 1 and \
               node.children[0] in ('None', 'True', 'False'):
            node['classes'].append('xref')
        BaseTranslator.visit_literal(self, node)

    def visit_productionlist(self, node):
        self.body.append(self.starttag(node, 'pre'))
        names = []
        for production in node:
            names.append(production['tokenname'])
        maxlen = max(len(name) for name in names)
        for production in node:
            if production['tokenname']:
                self.body.append(self.starttag(production, 'strong', ''))
                self.body.append(production['tokenname'].ljust(maxlen) +
                                 '</strong> ::= ')
                lastname = production['tokenname']
            else:
                self.body.append('%s     ' % (' '*len(lastname)))
            production.walkabout(self)
            self.body.append('\n')
        self.body.append('</pre>\n')
        raise nodes.SkipNode
    def depart_productionlist(self, node):
        pass

    def visit_production(self, node):
        pass
    def depart_production(self, node):
        pass

    def visit_centered(self, node):
        self.body.append(self.starttag(node, 'p', CLASS="centered") + '<strong>')
    def depart_centered(self, node):
        self.body.append('</strong></p>')

    def visit_compact_paragraph(self, node):
        pass
    def depart_compact_paragraph(self, node):
        pass

    def visit_highlightlang(self, node):
        self.highlightlang = node['lang']
        self.highlightlinenothreshold = node['linenothreshold']
    def depart_highlightlang(self, node):
        pass

    # overwritten
    def visit_image(self, node):
        olduri = node['uri']
        # rewrite the URI if the environment knows about it
        if olduri in self.builder.env.images:
            node['uri'] = posixpath.join(self.builder.imgpath,
                                         self.builder.env.images[olduri][1])
        BaseTranslator.visit_image(self, node)

    def visit_toctree(self, node):
        # this only happens when formatting a toc from env.tocs -- in this
        # case we don't want to include the subtree
        raise nodes.SkipNode

    def visit_index(self, node):
        raise nodes.SkipNode

    def visit_tabular_col_spec(self, node):
        raise nodes.SkipNode

    def visit_glossary(self, node):
        pass
    def depart_glossary(self, node):
        pass

    def visit_acks(self, node):
        pass
    def depart_acks(self, node):
        pass

    def visit_module(self, node):
        pass
    def depart_module(self, node):
        pass

    # docutils 0.5 compatibility
    def visit_note(self, node):
        self.visit_admonition(node, 'note')
    def depart_note(self, node):
        self.depart_admonition(node)

    # docutils 0.5 compatibility
    def visit_warning(self, node):
        self.visit_admonition(node, 'warning')
    def depart_warning(self, node):
        self.depart_admonition(node)

    # these are only handled specially in the SmartyPantsHTMLTranslator
    def visit_literal_emphasis(self, node):
        return self.visit_emphasis(node)
    def depart_literal_emphasis(self, node):
        return self.depart_emphasis(node)

    def depart_title(self, node):
        close_tag = self.context[-1]
        if self.builder.name != 'htmlhelp' and \
               (close_tag.startswith('</h') or
                close_tag.startswith('</a></h')) and \
               node.parent.hasattr('ids') and node.parent['ids']:
            aname = node.parent['ids'][0]
            # add permalink anchor
            self.body.append(u'<a class="headerlink" href="#%s" ' % aname +
                             u'title="Permalink to this headline">\u00B6</a>')
        BaseTranslator.depart_title(self, node)
Пример #26
0
def test_lexer_options():
    bridge = PygmentsBridge("html")
    ret = bridge.highlight_block("//comment", "php", opts={"startinline": True})
    assert '<span class="c1">//comment</span>' in ret
Пример #27
0
def test_add_lexer(app):
    app.add_lexer('test', MyLexer())

    bridge = PygmentsBridge('html')
    ret = bridge.highlight_block('ab', 'test')
    assert '<span class="n">a</span>b' in ret
Пример #28
0
class HTMLTranslator(BaseTranslator):
    """
    Our custom HTML translator.
    """
    def __init__(self, builder, *args, **kwds):
        BaseTranslator.__init__(self, *args, **kwds)
        self.highlighter = PygmentsBridge('html',
                                          builder.config.pygments_style)
        self.no_smarty = 0
        self.builder = builder
        self.highlightlang = 'python'
        self.highlightlinenothreshold = sys.maxint

    def visit_desc(self, node):
        self.body.append(self.starttag(node, 'dl', CLASS=node['desctype']))

    def depart_desc(self, node):
        self.body.append('</dl>\n\n')

    def visit_desc_signature(self, node):
        # the id is set automatically
        self.body.append(self.starttag(node, 'dt'))
        # anchor for per-desc interactive data
        if node.parent['desctype'] != 'describe' and node['ids'] and node[
                'first']:
            self.body.append('<!--[%s]-->' % node['ids'][0])
        if node.parent['desctype'] in ('class', 'exception'):
            self.body.append('%s ' % node.parent['desctype'])

    def depart_desc_signature(self, node):
        if node['ids'] and self.builder.name != 'htmlhelp':
            self.body.append(
                u'<a class="headerlink" href="#%s" ' % node['ids'][0] +
                u'title="Permalink to this definition">\u00B6</a>')
        self.body.append('</dt>\n')

    def visit_desc_addname(self, node):
        self.body.append(self.starttag(node, 'tt', '', CLASS='descclassname'))

    def depart_desc_addname(self, node):
        self.body.append('</tt>')

    def visit_desc_type(self, node):
        pass

    def depart_desc_type(self, node):
        pass

    def visit_desc_name(self, node):
        self.body.append(self.starttag(node, 'tt', '', CLASS='descname'))

    def depart_desc_name(self, node):
        self.body.append('</tt>')

    def visit_desc_parameterlist(self, node):
        self.body.append('<big>(</big>')
        self.first_param = 1

    def depart_desc_parameterlist(self, node):
        self.body.append('<big>)</big>')

    def visit_desc_parameter(self, node):
        if not self.first_param:
            self.body.append(', ')
        else:
            self.first_param = 0
        if not node.hasattr('noemph'):
            self.body.append('<em>')

    def depart_desc_parameter(self, node):
        if not node.hasattr('noemph'):
            self.body.append('</em>')

    def visit_desc_optional(self, node):
        self.body.append('<span class="optional">[</span>')

    def depart_desc_optional(self, node):
        self.body.append('<span class="optional">]</span>')

    def visit_desc_annotation(self, node):
        self.body.append(self.starttag(node, 'em', CLASS='property'))

    def depart_desc_annotation(self, node):
        self.body.append('</em>')

    def visit_desc_content(self, node):
        self.body.append(self.starttag(node, 'dd', ''))

    def depart_desc_content(self, node):
        self.body.append('</dd>')

    def visit_refcount(self, node):
        self.body.append(self.starttag(node, 'em', '', CLASS='refcount'))

    def depart_refcount(self, node):
        self.body.append('</em>')

    def visit_versionmodified(self, node):
        self.body.append(self.starttag(node, 'p'))
        text = version_text[node['type']] % node['version']
        if len(node):
            text += ': '
        else:
            text += '.'
        self.body.append('<span class="versionmodified">%s</span>' % text)

    def depart_versionmodified(self, node):
        self.body.append('</p>\n')

    # overwritten
    def visit_reference(self, node):
        BaseTranslator.visit_reference(self, node)
        if node.hasattr('reftitle'):
            # ugly hack to add a title attribute
            starttag = self.body[-1]
            if not starttag.startswith('<a '):
                return
            self.body[-1] = '<a title="%s"' % self.attval(node['reftitle']) + \
                            starttag[2:]

    # overwritten -- we don't want source comments to show up in the HTML
    def visit_comment(self, node):
        raise nodes.SkipNode

    # overwritten
    def visit_admonition(self, node, name=''):
        self.body.append(
            self.starttag(node, 'div', CLASS=('admonition ' + name)))
        if name and name != 'seealso':
            node.insert(0, nodes.title(name, self.language.labels[name]))
        self.set_first_last(node)

    def visit_seealso(self, node):
        self.visit_admonition(node, 'seealso')

    def depart_seealso(self, node):
        self.depart_admonition(node)

    # overwritten (args/kwds due to docutils 0.4/0.5 incompatibility)
    def visit_title(self, node, *args, **kwds):
        # if we have a section we do our own processing in order
        # to have ids in the hN-tags and not in additional a-tags
        if isinstance(node.parent, nodes.section):
            h_level = self.section_level + self.initial_header_level - 1
            if node.parent.get('ids'):
                attrs = {'ids': node.parent['ids']}
            else:
                attrs = {}
            self.body.append(self.starttag(node, 'h%d' % h_level, '', **attrs))
            self.context.append('</h%d>\n' % h_level)
        else:
            BaseTranslator.visit_title(self, node, *args, **kwds)

    # overwritten
    def visit_literal_block(self, node):
        if node.rawsource != node.astext():
            # most probably a parsed-literal block -- don't highlight
            return BaseTranslator.visit_literal_block(self, node)
        lang = self.highlightlang
        linenos = node.rawsource.count(
            '\n') >= self.highlightlinenothreshold - 1
        if node.has_key('language'):
            # code-block directives
            lang = node['language']
        if node.has_key('linenos'):
            linenos = node['linenos']
        self.body.append(
            self.highlighter.highlight_block(node.rawsource, lang, linenos))
        raise nodes.SkipNode

    def visit_doctest_block(self, node):
        self.visit_literal_block(node)

    # overwritten
    def visit_literal(self, node):
        if len(node.children) == 1 and \
               node.children[0] in ('None', 'True', 'False'):
            node['classes'].append('xref')
        BaseTranslator.visit_literal(self, node)

    def visit_productionlist(self, node):
        self.body.append(self.starttag(node, 'pre'))
        names = []
        for production in node:
            names.append(production['tokenname'])
        maxlen = max(len(name) for name in names)
        for production in node:
            if production['tokenname']:
                self.body.append(self.starttag(production, 'strong', ''))
                self.body.append(production['tokenname'].ljust(maxlen) +
                                 '</strong> ::= ')
                lastname = production['tokenname']
            else:
                self.body.append('%s     ' % (' ' * len(lastname)))
            production.walkabout(self)
            self.body.append('\n')
        self.body.append('</pre>\n')
        raise nodes.SkipNode

    def depart_productionlist(self, node):
        pass

    def visit_production(self, node):
        pass

    def depart_production(self, node):
        pass

    def visit_centered(self, node):
        self.body.append(
            self.starttag(node, 'p', CLASS="centered") + '<strong>')

    def depart_centered(self, node):
        self.body.append('</strong></p>')

    def visit_compact_paragraph(self, node):
        pass

    def depart_compact_paragraph(self, node):
        pass

    def visit_highlightlang(self, node):
        self.highlightlang = node['lang']
        self.highlightlinenothreshold = node['linenothreshold']

    def depart_highlightlang(self, node):
        pass

    # overwritten
    def visit_image(self, node):
        olduri = node['uri']
        # rewrite the URI if the environment knows about it
        if olduri in self.builder.images:
            node['uri'] = posixpath.join(self.builder.imgpath,
                                         self.builder.images[olduri])
        BaseTranslator.visit_image(self, node)

    def visit_toctree(self, node):
        # this only happens when formatting a toc from env.tocs -- in this
        # case we don't want to include the subtree
        raise nodes.SkipNode

    def visit_index(self, node):
        raise nodes.SkipNode

    def visit_tabular_col_spec(self, node):
        raise nodes.SkipNode

    def visit_glossary(self, node):
        pass

    def depart_glossary(self, node):
        pass

    def visit_acks(self, node):
        pass

    def depart_acks(self, node):
        pass

    def visit_module(self, node):
        pass

    def depart_module(self, node):
        pass

    # docutils 0.5 compatibility
    def visit_note(self, node):
        self.visit_admonition(node, 'note')

    def depart_note(self, node):
        self.depart_admonition(node)

    # docutils 0.5 compatibility
    def visit_warning(self, node):
        self.visit_admonition(node, 'warning')

    def depart_warning(self, node):
        self.depart_admonition(node)

    # these are only handled specially in the SmartyPantsHTMLTranslator
    def visit_literal_emphasis(self, node):
        return self.visit_emphasis(node)

    def depart_literal_emphasis(self, node):
        return self.depart_emphasis(node)

    def depart_title(self, node):
        close_tag = self.context[-1]
        if self.builder.name != 'htmlhelp' and \
               (close_tag.startswith('</h') or
                close_tag.startswith('</a></h')) and \
               node.parent.hasattr('ids') and node.parent['ids']:
            aname = node.parent['ids'][0]
            # add permalink anchor
            self.body.append(u'<a class="headerlink" href="#%s" ' % aname +
                             u'title="Permalink to this headline">\u00B6</a>')
        BaseTranslator.depart_title(self, node)

    def unknown_visit(self, node):
        raise NotImplementedError("Unknown node: " + node.__class__.__name__)
Пример #29
0
 def __init__(self, dest, stylename, trim_doctest_flags=None):
     if trim_doctest_flags is not None:
         PygmentsBridge.__init__(self, dest, stylename, trim_doctest_flags)
     else:
         PygmentsBridge.__init__(self, dest, stylename)
     self.formatter = DocxFormatter
Пример #30
0
class StandaloneHTMLBuilder(Builder):
    """
    Builds standalone HTML docs.
    """
    name = 'html'
    format = 'html'
    epilog = __('The HTML pages are in %(outdir)s.')

    copysource = True
    allow_parallel = True
    out_suffix = '.html'
    link_suffix = '.html'  # 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 HTML help or Qt help: suppresses sidebar
    search = True  # for things like HTML help and Apple help: suppress search
    use_index = False
    download_support = True  # enable download role

    imgpath = None          # type: str
    domain_indices = []     # type: List[Tuple[str, Type[Index], List[Tuple[str, List[IndexEntry]]], bool]]  # NOQA

    def __init__(self, app: Sphinx) -> None:
        super().__init__(app)

        # CSS files
        self.css_files = []  # type: List[Dict[str, str]]

        # JS files
        self.script_files = []  # type: List[JavaScript]

    def init(self) -> 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) -> BuildInfo:
        return BuildInfo(self.config, self.tags, ['html'])

    def _get_translations_js(self) -> 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) -> Tuple[str, Dict]:
        return self.config.html_theme, self.config.html_theme_options

    def init_templates(self) -> 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) -> 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) -> 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: str, **kwargs: 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) -> 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: str, **kwargs: 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[nodes.NodeVisitor]:  # type: ignore
        if not html5_ready or self.config.html4_writer:
            return HTMLTranslator
        else:
            return HTML5Translator

    @property
    def math_renderer_name(self) -> 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) -> 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) -> List[str]:
        return self.config.html_extra_path + self.config.html_static_path

    def render_partial(self, node: Node) -> Dict[str, str]:
        """Utility: Render a lone doctree node."""
        if node is None:
            return {'fragment': ''}
        doc = new_document('<partial node>')
        doc.append(node)

        writer = HTMLWriter(self)
        return publish_parts(reader_name='doctree',
                             writer=writer,
                             source_class=DocTreeInput,
                             settings_overrides={'output_encoding': 'unicode'},
                             source=doc)

    def prepare_writing(self, docnames: 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,
            'html5_doctype': html5_ready and not self.config.html4_writer
        }
        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: str, body: str, metatags: 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: str, doctree: 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: str, doctree: 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) -> 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_extra_files)
        self.finish_tasks.add_task(self.write_buildinfo)

        # dump the search index
        self.handle_finish()

    def gen_indices(self) -> None:
        logger.info(bold(__('generating indices...')), nonl=True)

        # 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) -> None:
        # pages from extensions
        for pagelist in self.events.emit('html-collect-pages'):
            for pagename, context, template in pagelist:
                self.handle_page(pagename, context, template)

        logger.info(bold(__('writing additional pages...')), nonl=True)

        # additional pages from conf.py
        for pagename, template in self.config.html_additional_pages.items():
            logger.info(' ' + pagename, nonl=True)
            self.handle_page(pagename, {}, template)

        # the search page
        if self.search:
            logger.info(' search', nonl=True)
            self.handle_page('search', {}, 'search.html')

        # the opensearch xml file
        if self.config.html_use_opensearch and self.search:
            logger.info(' opensearch', nonl=True)
            fn = path.join(self.outdir, '_static', 'opensearch.xml')
            self.handle_page('opensearch', {}, 'opensearch.xml', outfilename=fn)

        logger.info('')

    def write_genindex(self) -> 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=True)

        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) -> None:
        for indexname, indexcls, content, collapse in self.domain_indices:
            indexcontext = {
                'indextitle': indexcls.localname,
                'content': content,
                'collapse_index': collapse,
            }
            logger.info(' ' + indexname, nonl=True)
            self.handle_page(indexname, indexcontext, 'domainindex.html')

    def copy_image_files(self) -> 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) -> None:
        def to_relpath(f: 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_static_files(self) -> 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)
                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:
                entry = path.join(self.confdir, self.config.html_logo)
                copy_asset(entry, path.join(self.outdir, '_static'))
            if self.config.html_favicon:
                entry = path.join(self.confdir, self.config.html_favicon)
                copy_asset(entry, path.join(self.outdir, '_static'))
            logger.info(__('done'))
        except OSError as err:
            logger.warning(__('cannot copy static file %r'), err)

    def copy_extra_files(self) -> 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)
                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) -> 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) -> None:
        # clean up theme stuff
        if self.theme:
            self.theme.cleanup()

    def post_process_images(self, doctree: 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: 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: str, doctree: nodes.document, title: 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: str, collapse: bool = True, **kwds) -> 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: str) -> str:
        return path.join(self.outdir, os_path(pagename) + self.out_suffix)

    def add_sidebars(self, pagename: str, ctx: Dict) -> None:
        def has_wildcard(pattern: 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: str, typ: str = None) -> str:
        return docname + self.link_suffix

    def handle_page(self, pagename: str, addctx: Dict, templatename: str = 'page.html',
                    outfilename: str = None, event_arg: Any = None) -> 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: str, resource: bool = False, baseuri: str = default_baseuri) -> str:  # NOQA
            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: 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: 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

        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: str, templatename: str,
                            ctx: Dict, event_arg: Any) -> None:
        pass

    def handle_finish(self) -> 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) -> 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) -> 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'))
Пример #31
0
class HTMLTranslator(BaseTranslator):
    """
    Our custom HTML translator.
    """

    def __init__(self, builder, *args, **kwds):
        BaseTranslator.__init__(self, *args, **kwds)
        self.highlighter = PygmentsBridge("html", builder.config.pygments_style)
        self.no_smarty = 0
        self.builder = builder
        self.highlightlang = builder.config.highlight_language
        self.highlightlinenothreshold = sys.maxint
        self.protect_literal_text = 0

    def visit_desc(self, node):
        self.body.append(self.starttag(node, "dl", CLASS=node["desctype"]))

    def depart_desc(self, node):
        self.body.append("</dl>\n\n")

    def visit_desc_signature(self, node):
        # the id is set automatically
        self.body.append(self.starttag(node, "dt"))
        # anchor for per-desc interactive data
        if node.parent["desctype"] != "describe" and node["ids"] and node["first"]:
            self.body.append("<!--[%s]-->" % node["ids"][0])
        if node.parent["desctype"] in ("class", "exception"):
            self.body.append("%s " % node.parent["desctype"])

    def depart_desc_signature(self, node):
        if node["ids"] and self.builder.add_definition_links:
            self.body.append(
                u'<a class="headerlink" href="#%s" ' % node["ids"][0]
                + u'title="%s">\u00B6</a>' % _("Permalink to this definition")
            )
        self.body.append("</dt>\n")

    def visit_desc_addname(self, node):
        self.body.append(self.starttag(node, "tt", "", CLASS="descclassname"))

    def depart_desc_addname(self, node):
        self.body.append("</tt>")

    def visit_desc_type(self, node):
        pass

    def depart_desc_type(self, node):
        pass

    def visit_desc_name(self, node):
        self.body.append(self.starttag(node, "tt", "", CLASS="descname"))

    def depart_desc_name(self, node):
        self.body.append("</tt>")

    def visit_desc_parameterlist(self, node):
        self.body.append("<big>(</big>")
        self.first_param = 1

    def depart_desc_parameterlist(self, node):
        self.body.append("<big>)</big>")

    def visit_desc_parameter(self, node):
        if not self.first_param:
            self.body.append(", ")
        else:
            self.first_param = 0
        if not node.hasattr("noemph"):
            self.body.append("<em>")

    def depart_desc_parameter(self, node):
        if not node.hasattr("noemph"):
            self.body.append("</em>")

    def visit_desc_optional(self, node):
        self.body.append('<span class="optional">[</span>')

    def depart_desc_optional(self, node):
        self.body.append('<span class="optional">]</span>')

    def visit_desc_annotation(self, node):
        self.body.append(self.starttag(node, "em", CLASS="property"))

    def depart_desc_annotation(self, node):
        self.body.append("</em>")

    def visit_desc_content(self, node):
        self.body.append(self.starttag(node, "dd", ""))

    def depart_desc_content(self, node):
        self.body.append("</dd>")

    def visit_refcount(self, node):
        self.body.append(self.starttag(node, "em", "", CLASS="refcount"))

    def depart_refcount(self, node):
        self.body.append("</em>")

    def visit_versionmodified(self, node):
        self.body.append(self.starttag(node, "p"))
        text = versionlabels[node["type"]] % node["version"]
        if len(node):
            text += ": "
        else:
            text += "."
        self.body.append('<span class="versionmodified">%s</span>' % text)

    def depart_versionmodified(self, node):
        self.body.append("</p>\n")

    # overwritten
    def visit_reference(self, node):
        BaseTranslator.visit_reference(self, node)
        if node.hasattr("reftitle"):
            # ugly hack to add a title attribute
            starttag = self.body[-1]
            if not starttag.startswith("<a "):
                return
            self.body[-1] = '<a title="%s"' % self.attval(node["reftitle"]) + starttag[2:]

    # overwritten -- we don't want source comments to show up in the HTML
    def visit_comment(self, node):
        raise nodes.SkipNode

    # overwritten
    def visit_admonition(self, node, name=""):
        self.body.append(self.starttag(node, "div", CLASS=("admonition " + name)))
        if name and name != "seealso":
            node.insert(0, nodes.title(name, admonitionlabels[name]))
        self.set_first_last(node)

    def visit_seealso(self, node):
        self.visit_admonition(node, "seealso")

    def depart_seealso(self, node):
        self.depart_admonition(node)

    # overwritten for docutils 0.4
    if hasattr(BaseTranslator, "start_tag_with_title"):

        def visit_section(self, node):
            # the 0.5 version, to get the id attribute in the <div> tag
            self.section_level += 1
            self.body.append(self.starttag(node, "div", CLASS="section"))

        def visit_title(self, node):
            # don't move the id attribute inside the <h> tag
            BaseTranslator.visit_title(self, node, move_ids=0)

    # overwritten
    def visit_literal_block(self, node):
        if node.rawsource != node.astext():
            # most probably a parsed-literal block -- don't highlight
            return BaseTranslator.visit_literal_block(self, node)
        lang = self.highlightlang
        linenos = node.rawsource.count("\n") >= self.highlightlinenothreshold - 1
        if node.has_key("language"):
            # code-block directives
            lang = node["language"]
        if node.has_key("linenos"):
            linenos = node["linenos"]
        highlighted = self.highlighter.highlight_block(node.rawsource, lang, linenos)
        starttag = self.starttag(node, "div", suffix="", CLASS="highlight-%s" % lang)
        self.body.append(starttag + highlighted + "</div>\n")
        raise nodes.SkipNode

    def visit_doctest_block(self, node):
        self.visit_literal_block(node)

    # overwritten
    def visit_literal(self, node):
        if len(node.children) == 1 and node.children[0] in ("None", "True", "False"):
            node["classes"].append("xref")
        self.body.append(self.starttag(node, "tt", "", CLASS="docutils literal"))
        self.protect_literal_text += 1

    def depart_literal(self, node):
        self.protect_literal_text -= 1
        self.body.append("</tt>")

    def visit_productionlist(self, node):
        self.body.append(self.starttag(node, "pre"))
        names = []
        for production in node:
            names.append(production["tokenname"])
        maxlen = max(len(name) for name in names)
        for production in node:
            if production["tokenname"]:
                lastname = production["tokenname"].ljust(maxlen)
                self.body.append(self.starttag(production, "strong", ""))
                self.body.append(lastname + "</strong> ::= ")
            else:
                self.body.append("%s     " % (" " * len(lastname)))
            production.walkabout(self)
            self.body.append("\n")
        self.body.append("</pre>\n")
        raise nodes.SkipNode

    def depart_productionlist(self, node):
        pass

    def visit_production(self, node):
        pass

    def depart_production(self, node):
        pass

    def visit_centered(self, node):
        self.body.append(self.starttag(node, "p", CLASS="centered") + "<strong>")

    def depart_centered(self, node):
        self.body.append("</strong></p>")

    def visit_compact_paragraph(self, node):
        pass

    def depart_compact_paragraph(self, node):
        pass

    def visit_highlightlang(self, node):
        self.highlightlang = node["lang"]
        self.highlightlinenothreshold = node["linenothreshold"]

    def depart_highlightlang(self, node):
        pass

    # overwritten
    def visit_image(self, node):
        olduri = node["uri"]
        # rewrite the URI if the environment knows about it
        if olduri in self.builder.images:
            node["uri"] = posixpath.join(self.builder.imgpath, self.builder.images[olduri])

        if node.has_key("scale"):
            if Image and not (node.has_key("width") and node.has_key("height")):
                try:
                    im = Image.open(os.path.join(self.builder.srcdir, olduri))
                except (
                    IOError,  # Source image can't be found or opened
                    UnicodeError,
                ):  # PIL doesn't like Unicode paths.
                    print olduri
                    pass
                else:
                    if not node.has_key("width"):
                        node["width"] = str(im.size[0])
                    if not node.has_key("height"):
                        node["height"] = str(im.size[1])
                    del im
        BaseTranslator.visit_image(self, node)

    def visit_toctree(self, node):
        # this only happens when formatting a toc from env.tocs -- in this
        # case we don't want to include the subtree
        raise nodes.SkipNode

    def visit_index(self, node):
        raise nodes.SkipNode

    def visit_tabular_col_spec(self, node):
        raise nodes.SkipNode

    def visit_glossary(self, node):
        pass

    def depart_glossary(self, node):
        pass

    def visit_acks(self, node):
        pass

    def depart_acks(self, node):
        pass

    def visit_module(self, node):
        pass

    def depart_module(self, node):
        pass

    def bulk_text_processor(self, text):
        return text

    # overwritten
    def visit_Text(self, node):
        text = node.astext()
        encoded = self.encode(text)
        if self.protect_literal_text:
            # moved here from base class's visit_literal to support
            # more formatting in literal nodes
            for token in self.words_and_spaces.findall(encoded):
                if token.strip():
                    # protect literal text from line wrapping
                    self.body.append('<span class="pre">%s</span>' % token)
                elif token in " \n":
                    # allow breaks at whitespace
                    self.body.append(token)
                else:
                    # protect runs of multiple spaces; the last one can wrap
                    self.body.append("&nbsp;" * (len(token) - 1) + " ")
        else:
            if self.in_mailto and self.settings.cloak_email_addresses:
                encoded = self.cloak_email(encoded)
            else:
                encoded = self.bulk_text_processor(encoded)
            self.body.append(encoded)

    # these are all for docutils 0.5 compatibility

    def visit_note(self, node):
        self.visit_admonition(node, "note")

    def depart_note(self, node):
        self.depart_admonition(node)

    def visit_warning(self, node):
        self.visit_admonition(node, "warning")

    def depart_warning(self, node):
        self.depart_admonition(node)

    def visit_attention(self, node):
        self.visit_admonition(node, "attention")

    def depart_attention(self, node):
        self.depart_admonition()

    def visit_caution(self, node):
        self.visit_admonition(node, "caution")

    def depart_caution(self, node):
        self.depart_admonition()

    def visit_danger(self, node):
        self.visit_admonition(node, "danger")

    def depart_danger(self, node):
        self.depart_admonition()

    def visit_error(self, node):
        self.visit_admonition(node, "error")

    def depart_error(self, node):
        self.depart_admonition()

    def visit_hint(self, node):
        self.visit_admonition(node, "hint")

    def depart_hint(self, node):
        self.depart_admonition()

    def visit_important(self, node):
        self.visit_admonition(node, "important")

    def depart_important(self, node):
        self.depart_admonition()

    def visit_tip(self, node):
        self.visit_admonition(node, "tip")

    def depart_tip(self, node):
        self.depart_admonition()

    # these are only handled specially in the SmartyPantsHTMLTranslator
    def visit_literal_emphasis(self, node):
        return self.visit_emphasis(node)

    def depart_literal_emphasis(self, node):
        return self.depart_emphasis(node)

    def depart_title(self, node):
        close_tag = self.context[-1]
        if (
            self.builder.add_header_links
            and (close_tag.startswith("</h") or close_tag.startswith("</a></h"))
            and node.parent.hasattr("ids")
            and node.parent["ids"]
        ):
            aname = node.parent["ids"][0]
            # add permalink anchor
            self.body.append(
                u'<a class="headerlink" href="#%s" ' % aname
                + u'title="%s">\u00B6</a>' % _("Permalink to this headline")
            )
        BaseTranslator.depart_title(self, node)

    def unknown_visit(self, node):
        raise NotImplementedError("Unknown node: " + node.__class__.__name__)
Пример #32
0
def test_lexer_options():
    bridge = PygmentsBridge('html')
    ret = bridge.highlight_block('//comment', 'php', opts={'startinline': True})
    assert '<span class="c1">//comment</span>' in ret
Пример #33
0
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')
Пример #34
0
def test_lexer_options():
    bridge = PygmentsBridge('html')
    ret = bridge.highlight_block('//comment',
                                 'php',
                                 opts={'startinline': True})
    assert '<span class="c1">//comment</span>' in ret
Пример #35
0
def test_add_lexer(app, status, warning):
    app.add_lexer("test", MyLexer())

    bridge = PygmentsBridge("html")
    ret = bridge.highlight_block("ab", "test")
    assert '<span class="n">a</span>b' in ret
Пример #36
0
SASS_DIR = os.path.join(BASE_DIR, "sphinx_library", "scss")
STATIC_DIR = CSS_DIR = os.path.join(BASE_DIR, "sphinx_library", "static")
CSS_DIR = os.path.join(STATIC_DIR, "library")


# -- COMPILE SCSS -------------------------------------------------------------

# Generate pygment themes into scss files.

disclaimer = """/*
 * This file is auto-generated by ``build_sass.py``.
 * Edit pygments themes directly in ``sphinx_library.theme``
 */
"""

light_highlighter = PygmentsBridge("html", "sphinx_library.theme.LibraryLight")
with open(os.path.join(SASS_DIR, "_pygments-light.scss"), "w") as f:
    f.write(disclaimer)
    f.write(light_highlighter.get_stylesheet())

dark_highlighter = PygmentsBridge("html", "sphinx_library.theme.LibraryDark")
with open(os.path.join(SASS_DIR, "_pygments-dark.scss"), "w") as f:
    f.write(disclaimer)
    f.write(dark_highlighter.get_stylesheet())

# Compile theme scss.
sass.compile(
    output_style="compressed",
    dirname=(SASS_DIR, CSS_DIR),
)
Пример #37
0
def _get_dark_style(app: sphinx.application.Sphinx) -> Style:
    # number_of_hours_spent_figuring_this_out = 7
    #
    # Hello human in the future! This next block of code needs a bit of a story, and
    # if you're going to touch it, remember to update the number above (or remove this
    # comment entirely).
    #
    # Hopefully, you know that Sphinx allows extensions and themes to add configuration
    # values via `app.add_config_value`. This usually lets users set those values from
    # `conf.py` while allowing the extension to read from it and utilise that information.
    # As any reasonable person who's written a Sphinx extension before, you would
    # expect the following to work:
    #
    #     dark_style = app.config.pygments_dark_style
    #
    # Turns out, no. How dare you expect things to just work!? That stuff just returns
    # the default value provided when calling `app.add_config_value`. Yes, even if you
    # set it in `conf.py`. Why? Good question. :)
    #
    # The logic in Sphinx literally looks it up in the same mapping as what was
    # manipulated by `add_config_value`, and there's no other spot where that value
    # gets manipulated. I spent a bunch of time debugging how that class works, and...
    # yea, I can't figure it out. There's multiple mappings floating around and bunch
    # of manipulation being done for all kinds of things.
    #
    # The only place on the config object where I was able to find the user-provided
    # value from `conf.py` is a private variable `self._raw_config`. Those values are
    # supposed to get added to self.__dict__[...], and generally be accessible through
    # the object's custom `__getattr__`.
    #
    # Anyway, after giving up on figuring out how to file a PR to fix this upstream, I
    # started looking for hacky ways to get this without reaching into private
    # variables. That quest led to a very simple conclusion: no, you can't do that.
    #
    # So, here we are: with the only option being to reach into the guts of the beast,
    # and pull out the specific thing that's needed. This is obviously fragile though,
    # so this is written with the assumption that any changes to Sphinx's config
    # object's internals would correspond to the originally expected behaviour working.
    # This is so that when any of Sphinx's internals change, this logic would basically
    # fall back to the original behaviour and also print a warning, so that hopefully
    # someone will report this. Maybe it'll all be fixed, and I can remove this whole
    # hack and this giant comment.

    # HACK: begins here
    dark_style = None
    try:
        if (hasattr(app.config, "_raw_config")
                and isinstance(app.config._raw_config, dict)
                and "pygments_dark_style" in app.config._raw_config):
            dark_style = app.config._raw_config["pygments_dark_style"]
    except (AttributeError, KeyError) as e:
        logger.warn(
            ("Furo could not determine the value of `pygments_dark_style`. "
             "Falling back to using the value provided by Sphinx.\n"
             "Caused by %s"),
            e,
        )

    if dark_style is None:
        dark_style = app.config.pygments_dark_style

    return PygmentsBridge("html", dark_style).formatter_args["style"]
Пример #38
0
 def __init__(self, dest='docx', stylename='sphinx',
               trim_doctest_flags=False):
  PygmentsBridge.__init__(self, dest, stylename, trim_doctest_flags)
  dest = "html"
  self.formatter = DocxFormatter