mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Add experimental HTML5 writer
This commit is contained in:
parent
e14b296ef0
commit
0ef9ac54f1
2
CHANGES
2
CHANGES
@ -77,6 +77,8 @@ Features added
|
||||
* #3476: setuptools: Support multiple builders
|
||||
* latex: merged cells in LaTeX tables allow code-blocks, lists, blockquotes...
|
||||
as do normal cells (refs: #3435)
|
||||
* HTML buildre uses experimental HTML5 writer if ``html_experimental_html5_builder`` is True
|
||||
and docutils 0.13 and newer is installed.
|
||||
|
||||
Bugs fixed
|
||||
----------
|
||||
|
@ -1088,6 +1088,11 @@ that use Sphinx's HTMLWriter class.
|
||||
|
||||
Output file base name for HTML help builder. Default is ``'pydoc'``.
|
||||
|
||||
.. confval:: html_experimental_html5_writer
|
||||
|
||||
Output is processed with HTML5 writer. This feature needs docutils 0.13 or newer. Default is ``False``.
|
||||
|
||||
.. versionadded:: 1.6
|
||||
|
||||
.. _applehelp-options:
|
||||
|
||||
|
@ -46,6 +46,7 @@ from sphinx.util import status_iterator, old_status_iterator, display_chunk
|
||||
from sphinx.util.tags import Tags
|
||||
from sphinx.util.osutil import ENOENT
|
||||
from sphinx.util.console import bold, darkgreen # type: ignore
|
||||
from sphinx.util.docutils import is_html5_writer_available
|
||||
from sphinx.util.i18n import find_catalog_source_files
|
||||
|
||||
if False:
|
||||
@ -623,24 +624,32 @@ class Sphinx(object):
|
||||
raise ExtensionError('Value for key %r must be a '
|
||||
'(visit, depart) function tuple' % key)
|
||||
translator = self._translators.get(key)
|
||||
translators = []
|
||||
if translator is not None:
|
||||
pass
|
||||
translators.append(translator)
|
||||
elif key == 'html':
|
||||
from sphinx.writers.html import HTMLTranslator as translator # type: ignore
|
||||
from sphinx.writers.html import HTMLTranslator
|
||||
translators.append(HTMLTranslator)
|
||||
if is_html5_writer_available():
|
||||
from sphinx.writers.html5 import HTML5Translator
|
||||
translators.append(HTML5Translator)
|
||||
elif key == 'latex':
|
||||
from sphinx.writers.latex import LaTeXTranslator as translator # type: ignore
|
||||
from sphinx.writers.latex import LaTeXTranslator
|
||||
translators.append(LaTeXTranslator)
|
||||
elif key == 'text':
|
||||
from sphinx.writers.text import TextTranslator as translator # type: ignore
|
||||
from sphinx.writers.text import TextTranslator
|
||||
translators.append(TextTranslator)
|
||||
elif key == 'man':
|
||||
from sphinx.writers.manpage import ManualPageTranslator as translator # type: ignore # NOQA
|
||||
from sphinx.writers.manpage import ManualPageTranslator
|
||||
translators.append(ManualPageTranslator)
|
||||
elif key == 'texinfo':
|
||||
from sphinx.writers.texinfo import TexinfoTranslator as translator # type: ignore # NOQA
|
||||
else:
|
||||
# ignore invalid keys for compatibility
|
||||
continue
|
||||
setattr(translator, 'visit_' + node.__name__, visit)
|
||||
if depart:
|
||||
setattr(translator, 'depart_' + node.__name__, depart)
|
||||
from sphinx.writers.texinfo import TexinfoTranslator
|
||||
translators.append(TexinfoTranslator)
|
||||
|
||||
for translator in translators:
|
||||
setattr(translator, 'visit_' + node.__name__, visit)
|
||||
if depart:
|
||||
setattr(translator, 'depart_' + node.__name__, depart)
|
||||
|
||||
def add_enumerable_node(self, node, figtype, title_getter=None, **kwds):
|
||||
# type: (nodes.Node, unicode, Callable, Any) -> None
|
||||
|
@ -34,6 +34,7 @@ from sphinx.util.i18n import format_date
|
||||
from sphinx.util.osutil import SEP, os_path, relative_uri, ensuredir, \
|
||||
movefile, copyfile
|
||||
from sphinx.util.nodes import inline_all_toctrees
|
||||
from sphinx.util.docutils import is_html5_writer_available, __version_info__
|
||||
from sphinx.util.fileutil import copy_asset
|
||||
from sphinx.util.matching import patmatch, Matcher, DOTFILES
|
||||
from sphinx.config import string_classes
|
||||
@ -55,6 +56,13 @@ if False:
|
||||
from sphinx.domains import Domain, Index # NOQA
|
||||
from sphinx.application import Sphinx # NOQA
|
||||
|
||||
# Experimental HTML5 Writer
|
||||
if is_html5_writer_available():
|
||||
from sphinx.writers.html5 import HTML5Translator, SmartyPantsHTML5Translator
|
||||
html5_ready = True
|
||||
else:
|
||||
html5_ready = False
|
||||
|
||||
#: the filename for the inventory of objects
|
||||
INVENTORY_FILENAME = 'objects.inv'
|
||||
#: the filename for the "last build" file (for serializing builders)
|
||||
@ -145,6 +153,12 @@ class StandaloneHTMLBuilder(Builder):
|
||||
self.script_files.append('_static/translations.js')
|
||||
self.use_index = self.get_builder_config('use_index', 'html')
|
||||
|
||||
if self.config.html_experimental_html5_writer and not html5_ready:
|
||||
self.app.warn(' '.join((
|
||||
'html_experimental_html5_writer is set, but current version is old.',
|
||||
'Docutils\' version should be or newer than 0.13, but %s.',
|
||||
)) % '.'.join(map(str, __version_info__)))
|
||||
|
||||
def _get_translations_js(self):
|
||||
# type: () -> unicode
|
||||
candidates = [path.join(dir, self.config.language,
|
||||
@ -188,10 +202,16 @@ class StandaloneHTMLBuilder(Builder):
|
||||
def init_translator_class(self):
|
||||
# type: () -> None
|
||||
if self.translator_class is None:
|
||||
if self.config.html_use_smartypants:
|
||||
self.translator_class = SmartyPantsHTMLTranslator
|
||||
if self.config.html_experimental_html5_writer and html5_ready:
|
||||
if self.config.html_use_smartypants:
|
||||
self.translator_class = SmartyPantsHTML5Translator
|
||||
else:
|
||||
self.translator_class = HTML5Translator
|
||||
else:
|
||||
self.translator_class = HTMLTranslator
|
||||
if self.config.html_use_smartypants:
|
||||
self.translator_class = SmartyPantsHTMLTranslator
|
||||
else:
|
||||
self.translator_class = HTMLTranslator
|
||||
|
||||
def get_outdated_docs(self):
|
||||
# type: () -> Iterator[unicode]
|
||||
@ -374,6 +394,7 @@ class StandaloneHTMLBuilder(Builder):
|
||||
parents = [],
|
||||
logo = logo,
|
||||
favicon = favicon,
|
||||
html5_doctype = self.config.html_experimental_html5_writer and html5_ready,
|
||||
) # type: Dict[unicode, Any]
|
||||
if self.theme:
|
||||
self.globalcontext.update(
|
||||
@ -1320,6 +1341,7 @@ def setup(app):
|
||||
app.add_config_value('html_search_options', {}, 'html')
|
||||
app.add_config_value('html_search_scorer', '', None)
|
||||
app.add_config_value('html_scaled_image_link', True, 'html')
|
||||
app.add_config_value('html_experimental_html5_writer', False, 'html')
|
||||
|
||||
return {
|
||||
'version': 'builtin',
|
||||
|
@ -7,10 +7,12 @@
|
||||
:copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS.
|
||||
:license: BSD, see LICENSE for details.
|
||||
#}
|
||||
{%- block doctype -%}
|
||||
{%- block doctype -%}{%- if html5_doctype %}
|
||||
<!DOCTYPE html>
|
||||
{%- else %}
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
{%- endblock %}
|
||||
{%- endif %}{%- endblock %}
|
||||
{%- set reldelim1 = reldelim1 is not defined and ' »' or reldelim1 %}
|
||||
{%- set reldelim2 = reldelim2 is not defined and ' |' or reldelim2 %}
|
||||
{%- set render_sidebar = (not embedded) and (not theme_nosidebar|tobool) and
|
||||
|
@ -150,3 +150,8 @@ class LoggingReporter(Reporter):
|
||||
def set_conditions(self, category, report_level, halt_level, debug=False):
|
||||
# type: (unicode, int, int, bool) -> None
|
||||
Reporter.set_conditions(self, category, report_level, halt_level, debug=debug)
|
||||
|
||||
|
||||
def is_html5_writer_available():
|
||||
# type: () -> bool
|
||||
return __version_info__ > (0, 13, 0)
|
||||
|
929
sphinx/writers/html5.py
Normal file
929
sphinx/writers/html5.py
Normal file
@ -0,0 +1,929 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
sphinx.writers.html5
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Experimental docutils writers for HTML5 handling Sphinx' custom nodes.
|
||||
|
||||
:copyright: Copyright 2007-2017 by the Sphinx team, see AUTHORS.
|
||||
:license: BSD, see LICENSE for details.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import posixpath
|
||||
import os
|
||||
|
||||
from six import string_types
|
||||
from docutils import nodes
|
||||
from docutils.writers.html5_polyglot import HTMLTranslator as BaseTranslator
|
||||
|
||||
from sphinx import addnodes
|
||||
from sphinx.locale import admonitionlabels, _
|
||||
from sphinx.util import logging
|
||||
from sphinx.util.images import get_image_size
|
||||
from sphinx.util.smartypants import sphinx_smarty_pants
|
||||
|
||||
if False:
|
||||
# For type annotation
|
||||
from typing import Any # NOQA
|
||||
from sphinx.builders.html import StandaloneHTMLBuilder # NOQA
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# A good overview of the purpose behind these classes can be found here:
|
||||
# http://www.arnebrodowski.de/blog/write-your-own-restructuredtext-writer.html
|
||||
|
||||
|
||||
class HTML5Translator(BaseTranslator):
|
||||
"""
|
||||
Our custom HTML translator.
|
||||
"""
|
||||
|
||||
def __init__(self, builder, *args, **kwds):
|
||||
# type: (StandaloneHTMLBuilder, Any, Any) -> None
|
||||
BaseTranslator.__init__(self, *args, **kwds)
|
||||
self.highlighter = builder.highlighter
|
||||
self.no_smarty = 0
|
||||
self.builder = builder
|
||||
self.highlightlang = self.highlightlang_base = \
|
||||
builder.config.highlight_language
|
||||
self.highlightopts = builder.config.highlight_options
|
||||
self.highlightlinenothreshold = sys.maxsize
|
||||
self.docnames = [builder.current_docname] # for singlehtml builder
|
||||
self.protect_literal_text = 0
|
||||
self.permalink_text = builder.config.html_add_permalinks
|
||||
# support backwards-compatible setting to a bool
|
||||
if not isinstance(self.permalink_text, string_types):
|
||||
self.permalink_text = self.permalink_text and u'\u00B6' or ''
|
||||
self.permalink_text = self.encode(self.permalink_text)
|
||||
self.secnumber_suffix = builder.config.html_secnumber_suffix
|
||||
self.param_separator = ''
|
||||
self.optional_param_level = 0
|
||||
self._table_row_index = 0
|
||||
self.required_params_left = 0
|
||||
|
||||
def visit_start_of_file(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
# only occurs in the single-file builder
|
||||
self.docnames.append(node['docname'])
|
||||
self.body.append('<span id="document-%s"></span>' % node['docname'])
|
||||
|
||||
def depart_start_of_file(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.docnames.pop()
|
||||
|
||||
def visit_desc(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append(self.starttag(node, 'dl', CLASS=node['objtype']))
|
||||
|
||||
def depart_desc(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append('</dl>\n\n')
|
||||
|
||||
def visit_desc_signature(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
# the id is set automatically
|
||||
self.body.append(self.starttag(node, 'dt'))
|
||||
# anchor for per-desc interactive data
|
||||
if node.parent['objtype'] != 'describe' \
|
||||
and node['ids'] and node['first']:
|
||||
self.body.append('<!--[%s]-->' % node['ids'][0])
|
||||
|
||||
def depart_desc_signature(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
if not node.get('is_multiline'):
|
||||
self.add_permalink_ref(node, _('Permalink to this definition'))
|
||||
self.body.append('</dt>\n')
|
||||
|
||||
def visit_desc_signature_line(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
pass
|
||||
|
||||
def depart_desc_signature_line(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
if node.get('add_permalink'):
|
||||
# the permalink info is on the parent desc_signature node
|
||||
self.add_permalink_ref(node.parent, _('Permalink to this definition'))
|
||||
self.body.append('<br />')
|
||||
|
||||
def visit_desc_addname(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append(self.starttag(node, 'code', '', CLASS='descclassname'))
|
||||
|
||||
def depart_desc_addname(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append('</code>')
|
||||
|
||||
def visit_desc_type(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
pass
|
||||
|
||||
def depart_desc_type(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
pass
|
||||
|
||||
def visit_desc_returns(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append(' → ')
|
||||
|
||||
def depart_desc_returns(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
pass
|
||||
|
||||
def visit_desc_name(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append(self.starttag(node, 'code', '', CLASS='descname'))
|
||||
|
||||
def depart_desc_name(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append('</code>')
|
||||
|
||||
def visit_desc_parameterlist(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append('<span class="sig-paren">(</span>')
|
||||
self.first_param = 1
|
||||
self.optional_param_level = 0
|
||||
# How many required parameters are left.
|
||||
self.required_params_left = sum([isinstance(c, addnodes.desc_parameter)
|
||||
for c in node.children])
|
||||
self.param_separator = node.child_text_separator
|
||||
|
||||
def depart_desc_parameterlist(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append('<span class="sig-paren">)</span>')
|
||||
|
||||
# If required parameters are still to come, then put the comma after
|
||||
# the parameter. Otherwise, put the comma before. This ensures that
|
||||
# signatures like the following render correctly (see issue #1001):
|
||||
#
|
||||
# foo([a, ]b, c[, d])
|
||||
#
|
||||
def visit_desc_parameter(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
if self.first_param:
|
||||
self.first_param = 0
|
||||
elif not self.required_params_left:
|
||||
self.body.append(self.param_separator)
|
||||
if self.optional_param_level == 0:
|
||||
self.required_params_left -= 1
|
||||
if not node.hasattr('noemph'):
|
||||
self.body.append('<em>')
|
||||
|
||||
def depart_desc_parameter(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
if not node.hasattr('noemph'):
|
||||
self.body.append('</em>')
|
||||
if self.required_params_left:
|
||||
self.body.append(self.param_separator)
|
||||
|
||||
def visit_desc_optional(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.optional_param_level += 1
|
||||
self.body.append('<span class="optional">[</span>')
|
||||
|
||||
def depart_desc_optional(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.optional_param_level -= 1
|
||||
self.body.append('<span class="optional">]</span>')
|
||||
|
||||
def visit_desc_annotation(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append(self.starttag(node, 'em', '', CLASS='property'))
|
||||
|
||||
def depart_desc_annotation(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append('</em>')
|
||||
|
||||
def visit_desc_content(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append(self.starttag(node, 'dd', ''))
|
||||
|
||||
def depart_desc_content(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append('</dd>')
|
||||
|
||||
def visit_versionmodified(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append(self.starttag(node, 'div', CLASS=node['type']))
|
||||
|
||||
def depart_versionmodified(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append('</div>\n')
|
||||
|
||||
# overwritten
|
||||
def visit_reference(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
atts = {'class': 'reference'}
|
||||
if node.get('internal') or 'refuri' not in node:
|
||||
atts['class'] += ' internal'
|
||||
else:
|
||||
atts['class'] += ' external'
|
||||
if 'refuri' in node:
|
||||
atts['href'] = node['refuri'] or '#'
|
||||
if self.settings.cloak_email_addresses and \
|
||||
atts['href'].startswith('mailto:'):
|
||||
atts['href'] = self.cloak_mailto(atts['href'])
|
||||
self.in_mailto = 1
|
||||
else:
|
||||
assert 'refid' in node, \
|
||||
'References must have "refuri" or "refid" attribute.'
|
||||
atts['href'] = '#' + node['refid']
|
||||
if not isinstance(node.parent, nodes.TextElement):
|
||||
assert len(node) == 1 and isinstance(node[0], nodes.image)
|
||||
atts['class'] += ' image-reference'
|
||||
if 'reftitle' in node:
|
||||
atts['title'] = node['reftitle']
|
||||
if 'target' in node:
|
||||
atts['target'] = node['target']
|
||||
self.body.append(self.starttag(node, 'a', '', **atts))
|
||||
|
||||
if node.get('secnumber'):
|
||||
self.body.append(('%s' + self.secnumber_suffix) %
|
||||
'.'.join(map(str, node['secnumber'])))
|
||||
|
||||
def visit_number_reference(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.visit_reference(node)
|
||||
|
||||
def depart_number_reference(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.depart_reference(node)
|
||||
|
||||
# overwritten -- we don't want source comments to show up in the HTML
|
||||
def visit_comment(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
raise nodes.SkipNode
|
||||
|
||||
# overwritten
|
||||
def visit_admonition(self, node, name=''):
|
||||
# type: (nodes.Node, unicode) -> None
|
||||
self.body.append(self.starttag(
|
||||
node, 'div', CLASS=('admonition ' + name)))
|
||||
if name:
|
||||
node.insert(0, nodes.title(name, admonitionlabels[name]))
|
||||
|
||||
def visit_seealso(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.visit_admonition(node, 'seealso')
|
||||
|
||||
def depart_seealso(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.depart_admonition(node)
|
||||
|
||||
def add_secnumber(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
if node.get('secnumber'):
|
||||
self.body.append('.'.join(map(str, node['secnumber'])) +
|
||||
self.secnumber_suffix)
|
||||
elif isinstance(node.parent, nodes.section):
|
||||
if self.builder.name == 'singlehtml':
|
||||
docname = self.docnames[-1]
|
||||
anchorname = "%s/#%s" % (docname, node.parent['ids'][0])
|
||||
if anchorname not in self.builder.secnumbers:
|
||||
anchorname = "%s/" % docname # try first heading which has no anchor
|
||||
else:
|
||||
anchorname = '#' + node.parent['ids'][0]
|
||||
if anchorname not in self.builder.secnumbers:
|
||||
anchorname = '' # try first heading which has no anchor
|
||||
if self.builder.secnumbers.get(anchorname):
|
||||
numbers = self.builder.secnumbers[anchorname]
|
||||
self.body.append('.'.join(map(str, numbers)) +
|
||||
self.secnumber_suffix)
|
||||
|
||||
def add_fignumber(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
def append_fignumber(figtype, figure_id):
|
||||
# type: (unicode, unicode) -> None
|
||||
if self.builder.name == 'singlehtml':
|
||||
key = u"%s/%s" % (self.docnames[-1], figtype)
|
||||
else:
|
||||
key = figtype
|
||||
|
||||
if figure_id in self.builder.fignumbers.get(key, {}):
|
||||
self.body.append('<span class="caption-number">')
|
||||
prefix = self.builder.config.numfig_format.get(figtype)
|
||||
if prefix is None:
|
||||
msg = 'numfig_format is not defined for %s' % figtype
|
||||
logger.warning(msg)
|
||||
else:
|
||||
numbers = self.builder.fignumbers[key][figure_id]
|
||||
self.body.append(prefix % '.'.join(map(str, numbers)) + ' ')
|
||||
self.body.append('</span>')
|
||||
|
||||
figtype = self.builder.env.domains['std'].get_figtype(node) # type: ignore
|
||||
if figtype:
|
||||
if len(node['ids']) == 0:
|
||||
msg = 'Any IDs not assigned for %s node' % node.tagname
|
||||
logger.warning(msg, location=node)
|
||||
else:
|
||||
append_fignumber(figtype, node['ids'][0])
|
||||
|
||||
def add_permalink_ref(self, node, title):
|
||||
# type: (nodes.Node, unicode) -> None
|
||||
if node['ids'] and self.permalink_text and self.builder.add_permalinks:
|
||||
format = u'<a class="headerlink" href="#%s" title="%s">%s</a>'
|
||||
self.body.append(format % (node['ids'][0], title, self.permalink_text))
|
||||
|
||||
def generate_targets_for_listing(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
"""Generate hyperlink targets for listings.
|
||||
|
||||
Original visit_bullet_list(), visit_definition_list() and visit_enumerated_list()
|
||||
generates hyperlink targets inside listing tags (<ul>, <ol> and <dl>) if multiple
|
||||
IDs are assigned to listings. That is invalid DOM structure.
|
||||
(This is a bug of docutils <= 0.12)
|
||||
|
||||
This exports hyperlink targets before listings to make valid DOM structure.
|
||||
"""
|
||||
for id in node['ids'][1:]:
|
||||
self.body.append('<span id="%s"></span>' % id)
|
||||
node['ids'].remove(id)
|
||||
|
||||
# overwritten
|
||||
def visit_bullet_list(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
if len(node) == 1 and node[0].tagname == 'toctree':
|
||||
# avoid emitting empty <ul></ul>
|
||||
raise nodes.SkipNode
|
||||
self.generate_targets_for_listing(node)
|
||||
BaseTranslator.visit_bullet_list(self, node)
|
||||
|
||||
# overwritten
|
||||
def visit_enumerated_list(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.generate_targets_for_listing(node)
|
||||
BaseTranslator.visit_enumerated_list(self, node)
|
||||
|
||||
# overwritten
|
||||
def visit_title(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
BaseTranslator.visit_title(self, node)
|
||||
self.add_secnumber(node)
|
||||
self.add_fignumber(node.parent)
|
||||
if isinstance(node.parent, nodes.table):
|
||||
self.body.append('<span class="caption-text">')
|
||||
|
||||
def depart_title(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
close_tag = self.context[-1]
|
||||
if (self.permalink_text and self.builder.add_permalinks and
|
||||
node.parent.hasattr('ids') and node.parent['ids']):
|
||||
# add permalink anchor
|
||||
if close_tag.startswith('</h'):
|
||||
self.add_permalink_ref(node.parent, _('Permalink to this headline'))
|
||||
elif close_tag.startswith('</a></h'):
|
||||
self.body.append(u'</a><a class="headerlink" href="#%s" ' %
|
||||
node.parent['ids'][0] +
|
||||
u'title="%s">%s' % (
|
||||
_('Permalink to this headline'),
|
||||
self.permalink_text))
|
||||
elif isinstance(node.parent, nodes.table):
|
||||
self.body.append('</span>')
|
||||
self.add_permalink_ref(node.parent, _('Permalink to this table'))
|
||||
elif isinstance(node.parent, nodes.table):
|
||||
self.body.append('</span>')
|
||||
|
||||
BaseTranslator.depart_title(self, node)
|
||||
|
||||
# overwritten
|
||||
def visit_literal_block(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
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
|
||||
highlight_args = node.get('highlight_args', {})
|
||||
if 'language' in node:
|
||||
# code-block directives
|
||||
lang = node['language']
|
||||
highlight_args['force'] = True
|
||||
if 'linenos' in node:
|
||||
linenos = node['linenos']
|
||||
if lang is self.highlightlang_base:
|
||||
# only pass highlighter options for original language
|
||||
opts = self.highlightopts
|
||||
else:
|
||||
opts = {}
|
||||
|
||||
highlighted = self.highlighter.highlight_block(
|
||||
node.rawsource, lang, opts=opts, linenos=linenos,
|
||||
location=(self.builder.current_docname, node.line), **highlight_args
|
||||
)
|
||||
starttag = self.starttag(node, 'div', suffix='',
|
||||
CLASS='highlight-%s' % lang)
|
||||
self.body.append(starttag + highlighted + '</div>\n')
|
||||
raise nodes.SkipNode
|
||||
|
||||
def visit_caption(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
if isinstance(node.parent, nodes.container) and node.parent.get('literal_block'):
|
||||
self.body.append('<div class="code-block-caption">')
|
||||
else:
|
||||
BaseTranslator.visit_caption(self, node)
|
||||
self.add_fignumber(node.parent)
|
||||
self.body.append(self.starttag(node, 'span', '', CLASS='caption-text'))
|
||||
|
||||
def depart_caption(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append('</span>')
|
||||
|
||||
# append permalink if available
|
||||
if isinstance(node.parent, nodes.container) and node.parent.get('literal_block'):
|
||||
self.add_permalink_ref(node.parent, _('Permalink to this code'))
|
||||
elif isinstance(node.parent, nodes.figure):
|
||||
image_nodes = node.parent.traverse(nodes.image)
|
||||
target_node = image_nodes and image_nodes[0] or node.parent
|
||||
self.add_permalink_ref(target_node, _('Permalink to this image'))
|
||||
elif node.parent.get('toctree'):
|
||||
self.add_permalink_ref(node.parent.parent, _('Permalink to this toctree'))
|
||||
|
||||
if isinstance(node.parent, nodes.container) and node.parent.get('literal_block'):
|
||||
self.body.append('</div>\n')
|
||||
else:
|
||||
BaseTranslator.depart_caption(self, node)
|
||||
|
||||
def visit_doctest_block(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.visit_literal_block(node)
|
||||
|
||||
# overwritten to add the <div> (for XHTML compliance)
|
||||
def visit_block_quote(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append(self.starttag(node, 'blockquote') + '<div>')
|
||||
|
||||
def depart_block_quote(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append('</div></blockquote>\n')
|
||||
|
||||
# overwritten
|
||||
def visit_literal(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append(self.starttag(node, 'code', '',
|
||||
CLASS='docutils literal'))
|
||||
self.protect_literal_text += 1
|
||||
|
||||
def depart_literal(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.protect_literal_text -= 1
|
||||
self.body.append('</code>')
|
||||
|
||||
def visit_productionlist(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append(self.starttag(node, 'pre'))
|
||||
names = []
|
||||
for production in node:
|
||||
names.append(production['tokenname'])
|
||||
maxlen = max(len(name) for name in names)
|
||||
lastname = None
|
||||
for production in node:
|
||||
if production['tokenname']:
|
||||
lastname = production['tokenname'].ljust(maxlen)
|
||||
self.body.append(self.starttag(production, 'strong', ''))
|
||||
self.body.append(lastname + '</strong> ::= ')
|
||||
elif lastname is not None:
|
||||
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):
|
||||
# type: (nodes.Node) -> None
|
||||
pass
|
||||
|
||||
def visit_production(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
pass
|
||||
|
||||
def depart_production(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
pass
|
||||
|
||||
def visit_centered(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append(self.starttag(node, 'p', CLASS="centered") +
|
||||
'<strong>')
|
||||
|
||||
def depart_centered(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append('</strong></p>')
|
||||
|
||||
# overwritten
|
||||
def should_be_compact_paragraph(self, node):
|
||||
# type: (nodes.Node) -> bool
|
||||
"""Determine if the <p> tags around paragraph can be omitted."""
|
||||
if isinstance(node.parent, addnodes.desc_content):
|
||||
# Never compact desc_content items.
|
||||
return False
|
||||
if isinstance(node.parent, addnodes.versionmodified):
|
||||
# Never compact versionmodified nodes.
|
||||
return False
|
||||
return BaseTranslator.should_be_compact_paragraph(self, node)
|
||||
|
||||
def visit_compact_paragraph(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
pass
|
||||
|
||||
def depart_compact_paragraph(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
pass
|
||||
|
||||
def visit_highlightlang(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.highlightlang = node['lang']
|
||||
self.highlightlinenothreshold = node['linenothreshold']
|
||||
|
||||
def depart_highlightlang(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
pass
|
||||
|
||||
def visit_download_reference(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
if self.builder.download_support and node.hasattr('filename'):
|
||||
self.body.append(
|
||||
'<a class="reference download internal" href="%s" download="">' %
|
||||
posixpath.join(self.builder.dlpath, node['filename']))
|
||||
self.context.append('</a>')
|
||||
else:
|
||||
self.context.append('')
|
||||
|
||||
def depart_download_reference(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append(self.context.pop())
|
||||
|
||||
# overwritten
|
||||
def visit_image(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
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])
|
||||
|
||||
uri = node['uri']
|
||||
if uri.lower().endswith(('svg', 'svgz')):
|
||||
atts = {'src': uri}
|
||||
if 'width' in node:
|
||||
atts['width'] = node['width']
|
||||
if 'height' in node:
|
||||
atts['height'] = node['height']
|
||||
atts['alt'] = node.get('alt', uri)
|
||||
if 'align' in node:
|
||||
self.body.append('<div align="%s" class="align-%s">' %
|
||||
(node['align'], node['align']))
|
||||
self.context.append('</div>\n')
|
||||
else:
|
||||
self.context.append('')
|
||||
self.body.append(self.emptytag(node, 'img', '', **atts))
|
||||
return
|
||||
|
||||
if 'scale' in node:
|
||||
# Try to figure out image height and width. Docutils does that too,
|
||||
# but it tries the final file name, which does not necessarily exist
|
||||
# yet at the time the HTML file is written.
|
||||
if not ('width' in node and 'height' in node):
|
||||
size = get_image_size(os.path.join(self.builder.srcdir, olduri))
|
||||
if size is None:
|
||||
logger.warning('Could not obtain image size. :scale: option is ignored.',
|
||||
location=node)
|
||||
else:
|
||||
if 'width' not in node:
|
||||
node['width'] = str(size[0])
|
||||
if 'height' not in node:
|
||||
node['height'] = str(size[1])
|
||||
BaseTranslator.visit_image(self, node)
|
||||
|
||||
# overwritten
|
||||
def depart_image(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
if node['uri'].lower().endswith(('svg', 'svgz')):
|
||||
self.body.append(self.context.pop())
|
||||
else:
|
||||
BaseTranslator.depart_image(self, node)
|
||||
|
||||
def visit_toctree(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
# 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):
|
||||
# type: (nodes.Node) -> None
|
||||
raise nodes.SkipNode
|
||||
|
||||
def visit_tabular_col_spec(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
raise nodes.SkipNode
|
||||
|
||||
def visit_glossary(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
pass
|
||||
|
||||
def depart_glossary(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
pass
|
||||
|
||||
def visit_acks(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
pass
|
||||
|
||||
def depart_acks(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
pass
|
||||
|
||||
def visit_hlist(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append('<table class="hlist"><tr>')
|
||||
|
||||
def depart_hlist(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append('</tr></table>\n')
|
||||
|
||||
def visit_hlistcol(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append('<td>')
|
||||
|
||||
def depart_hlistcol(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append('</td>')
|
||||
|
||||
def bulk_text_processor(self, text):
|
||||
# type: (unicode) -> unicode
|
||||
return text
|
||||
|
||||
# overwritten
|
||||
def visit_Text(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
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(' ' * (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)
|
||||
|
||||
def visit_note(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.visit_admonition(node, 'note')
|
||||
|
||||
def depart_note(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.depart_admonition(node)
|
||||
|
||||
def visit_warning(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.visit_admonition(node, 'warning')
|
||||
|
||||
def depart_warning(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.depart_admonition(node)
|
||||
|
||||
def visit_attention(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.visit_admonition(node, 'attention')
|
||||
|
||||
def depart_attention(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.depart_admonition()
|
||||
|
||||
def visit_caution(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.visit_admonition(node, 'caution')
|
||||
|
||||
def depart_caution(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.depart_admonition()
|
||||
|
||||
def visit_danger(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.visit_admonition(node, 'danger')
|
||||
|
||||
def depart_danger(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.depart_admonition()
|
||||
|
||||
def visit_error(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.visit_admonition(node, 'error')
|
||||
|
||||
def depart_error(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.depart_admonition()
|
||||
|
||||
def visit_hint(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.visit_admonition(node, 'hint')
|
||||
|
||||
def depart_hint(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.depart_admonition()
|
||||
|
||||
def visit_important(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.visit_admonition(node, 'important')
|
||||
|
||||
def depart_important(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.depart_admonition()
|
||||
|
||||
def visit_tip(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.visit_admonition(node, 'tip')
|
||||
|
||||
def depart_tip(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.depart_admonition()
|
||||
|
||||
# these are only handled specially in the SmartyPantsHTML5Translator
|
||||
def visit_literal_emphasis(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
return self.visit_emphasis(node)
|
||||
|
||||
def depart_literal_emphasis(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
return self.depart_emphasis(node)
|
||||
|
||||
def visit_literal_strong(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
return self.visit_strong(node)
|
||||
|
||||
def depart_literal_strong(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
return self.depart_strong(node)
|
||||
|
||||
def visit_abbreviation(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
attrs = {}
|
||||
if node.hasattr('explanation'):
|
||||
attrs['title'] = node['explanation']
|
||||
self.body.append(self.starttag(node, 'abbr', '', **attrs))
|
||||
|
||||
def depart_abbreviation(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append('</abbr>')
|
||||
|
||||
def visit_manpage(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.visit_literal_emphasis(node)
|
||||
|
||||
def depart_manpage(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.depart_literal_emphasis(node)
|
||||
|
||||
# overwritten to add even/odd classes
|
||||
|
||||
def visit_table(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self._table_row_index = 0
|
||||
|
||||
classes = [cls.strip(u' \t\n')
|
||||
for cls in self.settings.table_style.split(',')]
|
||||
classes.insert(0, "docutils") # compat
|
||||
if 'align' in node:
|
||||
classes.append('align-%s' % node['align'])
|
||||
tag = self.starttag(node, 'table', CLASS=' '.join(classes))
|
||||
self.body.append(tag)
|
||||
|
||||
def visit_row(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self._table_row_index += 1
|
||||
if self._table_row_index % 2 == 0:
|
||||
node['classes'].append('row-even')
|
||||
else:
|
||||
node['classes'].append('row-odd')
|
||||
self.body.append(self.starttag(node, 'tr', ''))
|
||||
node.column = 0
|
||||
|
||||
def visit_field_list(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self._fieldlist_row_index = 0
|
||||
return BaseTranslator.visit_field_list(self, node)
|
||||
|
||||
def visit_field(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self._fieldlist_row_index += 1
|
||||
if self._fieldlist_row_index % 2 == 0:
|
||||
node['classes'].append('field-even')
|
||||
else:
|
||||
node['classes'].append('field-odd')
|
||||
return node
|
||||
|
||||
def visit_math(self, node, math_env=''):
|
||||
# type: (nodes.Node, unicode) -> None
|
||||
logger.warning('using "math" markup without a Sphinx math extension '
|
||||
'active, please use one of the math extensions '
|
||||
'described at http://sphinx-doc.org/ext/math.html',
|
||||
location=(self.builder.current_docname, node.line))
|
||||
raise nodes.SkipNode
|
||||
|
||||
def unknown_visit(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
raise NotImplementedError('Unknown node: ' + node.__class__.__name__)
|
||||
|
||||
|
||||
class SmartyPantsHTML5Translator(HTML5Translator):
|
||||
"""
|
||||
Handle ordinary text via smartypants, converting quotes and dashes
|
||||
to the correct entities.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwds):
|
||||
# type: (Any, Any) -> None
|
||||
self.no_smarty = 0
|
||||
HTML5Translator.__init__(self, *args, **kwds)
|
||||
|
||||
def visit_literal(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.no_smarty += 1
|
||||
try:
|
||||
# this raises SkipNode
|
||||
HTML5Translator.visit_literal(self, node)
|
||||
finally:
|
||||
self.no_smarty -= 1
|
||||
|
||||
def visit_literal_block(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.no_smarty += 1
|
||||
try:
|
||||
HTML5Translator.visit_literal_block(self, node)
|
||||
except nodes.SkipNode:
|
||||
# HTML5Translator raises SkipNode for simple literal blocks,
|
||||
# but not for parsed literal blocks
|
||||
self.no_smarty -= 1
|
||||
raise
|
||||
|
||||
def depart_literal_block(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
HTML5Translator.depart_literal_block(self, node)
|
||||
self.no_smarty -= 1
|
||||
|
||||
def visit_literal_emphasis(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.no_smarty += 1
|
||||
self.visit_emphasis(node)
|
||||
|
||||
def depart_literal_emphasis(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.depart_emphasis(node)
|
||||
self.no_smarty -= 1
|
||||
|
||||
def visit_literal_strong(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.no_smarty += 1
|
||||
self.visit_strong(node)
|
||||
|
||||
def depart_literal_strong(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.depart_strong(node)
|
||||
self.no_smarty -= 1
|
||||
|
||||
def visit_desc_signature(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.no_smarty += 1
|
||||
HTML5Translator.visit_desc_signature(self, node)
|
||||
|
||||
def depart_desc_signature(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.no_smarty -= 1
|
||||
HTML5Translator.depart_desc_signature(self, node)
|
||||
|
||||
def visit_productionlist(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.no_smarty += 1
|
||||
try:
|
||||
HTML5Translator.visit_productionlist(self, node)
|
||||
finally:
|
||||
self.no_smarty -= 1
|
||||
|
||||
def visit_option(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.no_smarty += 1
|
||||
HTML5Translator.visit_option(self, node)
|
||||
|
||||
def depart_option(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.no_smarty -= 1
|
||||
HTML5Translator.depart_option(self, node)
|
||||
|
||||
def bulk_text_processor(self, text):
|
||||
# type: (unicode) -> unicode
|
||||
if self.no_smarty <= 0:
|
||||
return sphinx_smarty_pants(text)
|
||||
return text
|
@ -109,9 +109,9 @@ def check_xpath(etree, fname, path, check, be_found=True):
|
||||
# Since pygments-2.1.1, empty <span> tag is inserted at top of
|
||||
# highlighting block
|
||||
if len(node) == 1 and node[0].tag == 'span' and node[0].text is None:
|
||||
return node[0].tail
|
||||
else:
|
||||
return ''
|
||||
if node[0].tail is not None:
|
||||
return node[0].tail
|
||||
return ''
|
||||
|
||||
rex = re.compile(check)
|
||||
if be_found:
|
||||
|
330
tests/test_build_html5.py
Normal file
330
tests/test_build_html5.py
Normal file
@ -0,0 +1,330 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
test_build_html5
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Test the HTML5 writer and check output against XPath.
|
||||
|
||||
This code is digest to reduce test running time.
|
||||
Complete test code is here:
|
||||
|
||||
https://github.com/sphinx-doc/sphinx/pull/2805/files
|
||||
|
||||
:copyright: Copyright 2007-2017 by the Sphinx team, see AUTHORS.
|
||||
:license: BSD, see LICENSE for details.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from itertools import cycle, chain
|
||||
import xml.etree.cElementTree as ElementTree
|
||||
|
||||
from six import PY3
|
||||
import pytest
|
||||
from html5lib import getTreeBuilder, HTMLParser
|
||||
|
||||
from sphinx import __display_version__
|
||||
from sphinx.util.docutils import is_html5_writer_available
|
||||
|
||||
from util import remove_unicode_literals, strip_escseq, skip_unless
|
||||
from test_build_html import flat_dict, tail_check, check_xpath
|
||||
|
||||
TREE_BUILDER = getTreeBuilder('etree', implementation=ElementTree)
|
||||
HTML_PARSER = HTMLParser(TREE_BUILDER, namespaceHTMLElements=False)
|
||||
|
||||
|
||||
etree_cache = {}
|
||||
|
||||
@skip_unless(is_html5_writer_available())
|
||||
@pytest.fixture(scope='module')
|
||||
def cached_etree_parse():
|
||||
def parse(fname):
|
||||
if fname in etree_cache:
|
||||
return etree_cache[fname]
|
||||
with (fname).open('rb') as fp:
|
||||
etree = HTML_PARSER.parse(fp)
|
||||
etree_cache.clear()
|
||||
etree_cache[fname] = etree
|
||||
return etree
|
||||
yield parse
|
||||
etree_cache.clear()
|
||||
|
||||
|
||||
@skip_unless(is_html5_writer_available())
|
||||
@pytest.mark.parametrize("fname,expect", flat_dict({
|
||||
'images.html': [
|
||||
(".//img[@src='_images/img.png']", ''),
|
||||
(".//img[@src='_images/img1.png']", ''),
|
||||
(".//img[@src='_images/simg.png']", ''),
|
||||
(".//img[@src='_images/svgimg.svg']", ''),
|
||||
(".//a[@href='_sources/images.txt']", ''),
|
||||
],
|
||||
'subdir/images.html': [
|
||||
(".//img[@src='../_images/img1.png']", ''),
|
||||
(".//img[@src='../_images/rimg.png']", ''),
|
||||
],
|
||||
'subdir/includes.html': [
|
||||
(".//a[@href='../_downloads/img.png']", ''),
|
||||
(".//img[@src='../_images/img.png']", ''),
|
||||
(".//p", 'This is an include file.'),
|
||||
(".//pre/span", 'line 1'),
|
||||
(".//pre/span", 'line 2'),
|
||||
],
|
||||
'includes.html': [
|
||||
(".//pre", u'Max Strauß'),
|
||||
(".//a[@href='_downloads/img.png']", ''),
|
||||
(".//a[@href='_downloads/img1.png']", ''),
|
||||
(".//pre/span", u'"quotes"'),
|
||||
(".//pre/span", u"'included'"),
|
||||
(".//pre/span[@class='s2']", u'üöä'),
|
||||
(".//div[@class='inc-pyobj1 highlight-text']//pre",
|
||||
r'^class Foo:\n pass\n\s*$'),
|
||||
(".//div[@class='inc-pyobj2 highlight-text']//pre",
|
||||
r'^ def baz\(\):\n pass\n\s*$'),
|
||||
(".//div[@class='inc-lines highlight-text']//pre",
|
||||
r'^class Foo:\n pass\nclass Bar:\n$'),
|
||||
(".//div[@class='inc-startend highlight-text']//pre",
|
||||
u'^foo = "Including Unicode characters: üöä"\\n$'),
|
||||
(".//div[@class='inc-preappend highlight-text']//pre",
|
||||
r'(?m)^START CODE$'),
|
||||
(".//div[@class='inc-pyobj-dedent highlight-python']//span",
|
||||
r'def'),
|
||||
(".//div[@class='inc-tab3 highlight-text']//pre",
|
||||
r'-| |-'),
|
||||
(".//div[@class='inc-tab8 highlight-python']//pre/span",
|
||||
r'-| |-'),
|
||||
],
|
||||
'autodoc.html': [
|
||||
(".//dt[@id='test_autodoc.Class']", ''),
|
||||
(".//dt[@id='test_autodoc.function']/em", r'\*\*kwds'),
|
||||
(".//dd/p", r'Return spam\.'),
|
||||
],
|
||||
'extapi.html': [
|
||||
(".//strong", 'from function: Foo'),
|
||||
(".//strong", 'from class: Bar'),
|
||||
],
|
||||
'markup.html': [
|
||||
(".//title", 'set by title directive'),
|
||||
(".//p/em", 'Section author: Georg Brandl'),
|
||||
(".//p/em", 'Module author: Georg Brandl'),
|
||||
# created by the meta directive
|
||||
(".//meta[@name='author'][@content='Me']", ''),
|
||||
(".//meta[@name='keywords'][@content='docs, sphinx']", ''),
|
||||
# a label created by ``.. _label:``
|
||||
(".//div[@id='label']", ''),
|
||||
# code with standard code blocks
|
||||
(".//pre", '^some code$'),
|
||||
# an option list
|
||||
(".//span[@class='option']", '--help'),
|
||||
# admonitions
|
||||
(".//p[@class='admonition-title']", 'My Admonition'),
|
||||
(".//div[@class='admonition note']/p", 'Note text.'),
|
||||
(".//div[@class='admonition warning']/p", 'Warning text.'),
|
||||
# inline markup
|
||||
(".//li/p/strong", r'^command\\n$'),
|
||||
(".//li/p/strong", r'^program\\n$'),
|
||||
(".//li/p/em", r'^dfn\\n$'),
|
||||
(".//li/p/code/span[@class='pre']", r'^kbd\\n$'),
|
||||
(".//li/p/span", u'File \N{TRIANGULAR BULLET} Close'),
|
||||
(".//li/p/code/span[@class='pre']", '^a/$'),
|
||||
(".//li/p/code/em/span[@class='pre']", '^varpart$'),
|
||||
(".//li/p/code/em/span[@class='pre']", '^i$'),
|
||||
(".//a[@href='https://www.python.org/dev/peps/pep-0008']"
|
||||
"[@class='pep reference external']/strong", 'PEP 8'),
|
||||
(".//a[@href='https://www.python.org/dev/peps/pep-0008']"
|
||||
"[@class='pep reference external']/strong",
|
||||
'Python Enhancement Proposal #8'),
|
||||
(".//a[@href='https://tools.ietf.org/html/rfc1.html']"
|
||||
"[@class='rfc reference external']/strong", 'RFC 1'),
|
||||
(".//a[@href='https://tools.ietf.org/html/rfc1.html']"
|
||||
"[@class='rfc reference external']/strong", 'Request for Comments #1'),
|
||||
(".//a[@href='objects.html#envvar-HOME']"
|
||||
"[@class='reference internal']/code/span[@class='pre']", 'HOME'),
|
||||
(".//a[@href='#with']"
|
||||
"[@class='reference internal']/code/span[@class='pre']", '^with$'),
|
||||
(".//a[@href='#grammar-token-try_stmt']"
|
||||
"[@class='reference internal']/code/span", '^statement$'),
|
||||
(".//a[@href='#some-label'][@class='reference internal']/span", '^here$'),
|
||||
(".//a[@href='#some-label'][@class='reference internal']/span", '^there$'),
|
||||
(".//a[@href='subdir/includes.html']"
|
||||
"[@class='reference internal']/span", 'Including in subdir'),
|
||||
(".//a[@href='objects.html#cmdoption-python-c']"
|
||||
"[@class='reference internal']/code/span[@class='pre']", '-c'),
|
||||
# abbreviations
|
||||
(".//abbr[@title='abbreviation']", '^abbr$'),
|
||||
# version stuff
|
||||
(".//div[@class='versionadded']/p/span", 'New in version 0.6: '),
|
||||
(".//div[@class='versionadded']/p/span",
|
||||
tail_check('First paragraph of versionadded')),
|
||||
(".//div[@class='versionchanged']/p/span",
|
||||
tail_check('First paragraph of versionchanged')),
|
||||
(".//div[@class='versionchanged']/p",
|
||||
'Second paragraph of versionchanged'),
|
||||
# footnote reference
|
||||
(".//a[@class='footnote-reference brackets']", r'1'),
|
||||
# created by reference lookup
|
||||
(".//a[@href='contents.html#ref1']", ''),
|
||||
# ``seealso`` directive
|
||||
(".//div/p[@class='admonition-title']", 'See also'),
|
||||
# a ``hlist`` directive
|
||||
(".//table[@class='hlist']/tbody/tr/td/ul/li/p", '^This$'),
|
||||
# a ``centered`` directive
|
||||
(".//p[@class='centered']/strong", 'LICENSE'),
|
||||
# a glossary
|
||||
(".//dl/dt[@id='term-boson']", 'boson'),
|
||||
# a production list
|
||||
(".//pre/strong", 'try_stmt'),
|
||||
(".//pre/a[@href='#grammar-token-try1_stmt']/code/span", 'try1_stmt'),
|
||||
# tests for ``only`` directive
|
||||
(".//p", 'A global substitution.'),
|
||||
(".//p", 'In HTML.'),
|
||||
(".//p", 'In both.'),
|
||||
(".//p", 'Always present'),
|
||||
# tests for ``any`` role
|
||||
(".//a[@href='#with']/span", 'headings'),
|
||||
(".//a[@href='objects.html#func_without_body']/code/span", 'objects'),
|
||||
],
|
||||
'objects.html': [
|
||||
(".//dt[@id='mod.Cls.meth1']", ''),
|
||||
(".//dt[@id='errmod.Error']", ''),
|
||||
(".//dt/code", r'long\(parameter,\s* list\)'),
|
||||
(".//dt/code", 'another one'),
|
||||
(".//a[@href='#mod.Cls'][@class='reference internal']", ''),
|
||||
(".//dl[@class='userdesc']", ''),
|
||||
(".//dt[@id='userdesc-myobj']", ''),
|
||||
(".//a[@href='#userdesc-myobj'][@class='reference internal']", ''),
|
||||
# docfields
|
||||
(".//a[@class='reference internal'][@href='#TimeInt']/em", 'TimeInt'),
|
||||
(".//a[@class='reference internal'][@href='#Time']", 'Time'),
|
||||
(".//a[@class='reference internal'][@href='#errmod.Error']/strong", 'Error'),
|
||||
# C references
|
||||
(".//span[@class='pre']", 'CFunction()'),
|
||||
(".//a[@href='#c.Sphinx_DoSomething']", ''),
|
||||
(".//a[@href='#c.SphinxStruct.member']", ''),
|
||||
(".//a[@href='#c.SPHINX_USE_PYTHON']", ''),
|
||||
(".//a[@href='#c.SphinxType']", ''),
|
||||
(".//a[@href='#c.sphinx_global']", ''),
|
||||
# test global TOC created by toctree()
|
||||
(".//ul[@class='current']/li[@class='toctree-l1 current']/a[@href='#']",
|
||||
'Testing object descriptions'),
|
||||
(".//li[@class='toctree-l1']/a[@href='markup.html']",
|
||||
'Testing various markup'),
|
||||
# test unknown field names
|
||||
(".//dt[@class='field-odd']", 'Field_name'),
|
||||
(".//dt[@class='field-even']", 'Field_name all lower'),
|
||||
(".//dt[@class='field-odd']", 'FIELD_NAME'),
|
||||
(".//dt[@class='field-even']", 'FIELD_NAME ALL CAPS'),
|
||||
(".//dt[@class='field-odd']", 'Field_Name'),
|
||||
(".//dt[@class='field-even']", 'Field_Name All Word Caps'),
|
||||
(".//dt[@class='field-odd']", 'Field_name'),
|
||||
(".//dt[@class='field-even']", 'Field_name First word cap'),
|
||||
(".//dt[@class='field-odd']", 'FIELd_name'),
|
||||
(".//dt[@class='field-even']", 'FIELd_name PARTial caps'),
|
||||
# custom sidebar
|
||||
(".//h4", 'Custom sidebar'),
|
||||
# docfields
|
||||
(".//dd[@class='field-odd']/p/strong", '^moo$'),
|
||||
(".//dd[@class='field-odd']/p/strong", tail_check(r'\(Moo\) .* Moo')),
|
||||
(".//dd[@class='field-odd']/ul/li/p/strong", '^hour$'),
|
||||
(".//dd[@class='field-odd']/ul/li/p/em", '^DuplicateType$'),
|
||||
(".//dd[@class='field-odd']/ul/li/p/em", tail_check(r'.* Some parameter')),
|
||||
# others
|
||||
(".//a[@class='reference internal'][@href='#cmdoption-perl-arg-p']/code/span",
|
||||
'perl'),
|
||||
(".//a[@class='reference internal'][@href='#cmdoption-perl-arg-p']/code/span",
|
||||
'\\+p'),
|
||||
(".//a[@class='reference internal'][@href='#cmdoption-perl-objc']/code/span",
|
||||
'--ObjC\\+\\+'),
|
||||
(".//a[@class='reference internal'][@href='#cmdoption-perl-plugin-option']/code/span",
|
||||
'--plugin.option'),
|
||||
(".//a[@class='reference internal'][@href='#cmdoption-perl-arg-create-auth-token']"
|
||||
"/code/span",
|
||||
'create-auth-token'),
|
||||
(".//a[@class='reference internal'][@href='#cmdoption-perl-arg-arg']/code/span",
|
||||
'arg'),
|
||||
(".//a[@class='reference internal'][@href='#cmdoption-hg-arg-commit']/code/span",
|
||||
'hg'),
|
||||
(".//a[@class='reference internal'][@href='#cmdoption-hg-arg-commit']/code/span",
|
||||
'commit'),
|
||||
(".//a[@class='reference internal'][@href='#cmdoption-git-commit-p']/code/span",
|
||||
'git'),
|
||||
(".//a[@class='reference internal'][@href='#cmdoption-git-commit-p']/code/span",
|
||||
'commit'),
|
||||
(".//a[@class='reference internal'][@href='#cmdoption-git-commit-p']/code/span",
|
||||
'-p'),
|
||||
],
|
||||
'contents.html': [
|
||||
(".//meta[@name='hc'][@content='hcval']", ''),
|
||||
(".//meta[@name='hc_co'][@content='hcval_co']", ''),
|
||||
(".//meta[@name='testopt'][@content='testoverride']", ''),
|
||||
(".//dt[@class='label']/span[@class='brackets']", r'Ref1'),
|
||||
(".//dt[@class='label']", ''),
|
||||
(".//li[@class='toctree-l1']/a", 'Testing various markup'),
|
||||
(".//li[@class='toctree-l2']/a", 'Inline markup'),
|
||||
(".//title", 'Sphinx <Tests>'),
|
||||
(".//div[@class='footer']", 'Georg Brandl & Team'),
|
||||
(".//a[@href='http://python.org/']"
|
||||
"[@class='reference external']", ''),
|
||||
(".//li/p/a[@href='genindex.html']/span", 'Index'),
|
||||
(".//li/p/a[@href='py-modindex.html']/span", 'Module Index'),
|
||||
(".//li/p/a[@href='search.html']/span", 'Search Page'),
|
||||
# custom sidebar only for contents
|
||||
(".//h4", 'Contents sidebar'),
|
||||
# custom JavaScript
|
||||
(".//script[@src='file://moo.js']", ''),
|
||||
# URL in contents
|
||||
(".//a[@class='reference external'][@href='http://sphinx-doc.org/']",
|
||||
'http://sphinx-doc.org/'),
|
||||
(".//a[@class='reference external'][@href='http://sphinx-doc.org/latest/']",
|
||||
'Latest reference'),
|
||||
# Indirect hyperlink targets across files
|
||||
(".//a[@href='markup.html#some-label'][@class='reference internal']/span",
|
||||
'^indirect hyperref$'),
|
||||
],
|
||||
'bom.html': [
|
||||
(".//title", " File with UTF-8 BOM"),
|
||||
],
|
||||
'extensions.html': [
|
||||
(".//a[@href='http://python.org/dev/']", "http://python.org/dev/"),
|
||||
(".//a[@href='http://bugs.python.org/issue1000']", "issue 1000"),
|
||||
(".//a[@href='http://bugs.python.org/issue1042']", "explicit caption"),
|
||||
],
|
||||
'_static/statictmpl.html': [
|
||||
(".//project", 'Sphinx <Tests>'),
|
||||
],
|
||||
'genindex.html': [
|
||||
# index entries
|
||||
(".//a/strong", "Main"),
|
||||
(".//a/strong", "[1]"),
|
||||
(".//a/strong", "Other"),
|
||||
(".//a", "entry"),
|
||||
(".//li/a", "double"),
|
||||
],
|
||||
'footnote.html': [
|
||||
(".//a[@class='footnote-reference brackets'][@href='#id8'][@id='id1']", r"1"),
|
||||
(".//a[@class='footnote-reference brackets'][@href='#id9'][@id='id2']", r"2"),
|
||||
(".//a[@class='footnote-reference brackets'][@href='#foo'][@id='id3']", r"3"),
|
||||
(".//a[@class='reference internal'][@href='#bar'][@id='id4']", r"\[bar\]"),
|
||||
(".//a[@class='footnote-reference brackets'][@href='#id10'][@id='id5']", r"4"),
|
||||
(".//a[@class='footnote-reference brackets'][@href='#id11'][@id='id6']", r"5"),
|
||||
(".//a[@class='fn-backref'][@href='#id1']", r"1"),
|
||||
(".//a[@class='fn-backref'][@href='#id2']", r"2"),
|
||||
(".//a[@class='fn-backref'][@href='#id3']", r"3"),
|
||||
(".//a[@class='fn-backref'][@href='#id4']", r"bar"),
|
||||
(".//a[@class='fn-backref'][@href='#id5']", r"4"),
|
||||
(".//a[@class='fn-backref'][@href='#id6']", r"5"),
|
||||
(".//a[@class='fn-backref'][@href='#id7']", r"6"),
|
||||
],
|
||||
'otherext.html': [
|
||||
(".//h1", "Generated section"),
|
||||
(".//a[@href='_sources/otherext.foo.txt']", ''),
|
||||
]
|
||||
}))
|
||||
@pytest.mark.sphinx('html', tags=['testtag'], confoverrides={
|
||||
'html_context.hckey_co': 'hcval_co',
|
||||
'html_experimental_html5_writer': True})
|
||||
@pytest.mark.test_params(shared_result='test_build_html5_output')
|
||||
def test_html5_output(app, cached_etree_parse, fname, expect):
|
||||
app.build()
|
||||
print(app.outdir / fname)
|
||||
check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)
|
Loading…
Reference in New Issue
Block a user