diff --git a/CHANGES b/CHANGES index fd40e17dc..20585e834 100644 --- a/CHANGES +++ b/CHANGES @@ -23,6 +23,7 @@ Dependencies - sphinxcontrib.applehelp - sphinxcontrib.devhelp + - sphinxcontrib.htmlhelp - sphinxcontrib.jsmath - sphinxcontrib.qthelp diff --git a/doc/extdev/index.rst b/doc/extdev/index.rst index 4cc868c77..7203ec6fa 100644 --- a/doc/extdev/index.rst +++ b/doc/extdev/index.rst @@ -295,6 +295,11 @@ The following is a list of deprecated interfaces. - 4.0 - ``sphinx.builders.singlehtml.SingleFileHTMLBuilder`` + * - ``sphinx.builders.htmlhelp`` + - 2.0 + - 4.0 + - ``sphinxcontrib.htmlhelp`` + * - ``sphinx.builders.htmlhelp.HTMLHelpBuilder.open_file()`` - 2.0 - 4.0 diff --git a/setup.py b/setup.py index 5b2c38e8c..cdcbbc4f7 100644 --- a/setup.py +++ b/setup.py @@ -18,6 +18,7 @@ install_requires = [ 'sphinxcontrib-applehelp', 'sphinxcontrib-devhelp', 'sphinxcontrib-jsmath', + 'sphinxcontrib-htmlhelp', 'sphinxcontrib-qthelp', 'Jinja2>=2.3', 'Pygments>=2.0', diff --git a/sphinx/application.py b/sphinx/application.py index dfeeea6dc..296104e21 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -67,7 +67,6 @@ builtin_extensions = ( 'sphinx.builders.dummy', 'sphinx.builders.gettext', 'sphinx.builders.html', - 'sphinx.builders.htmlhelp', 'sphinx.builders.latex', 'sphinx.builders.linkcheck', 'sphinx.builders.manpage', @@ -107,6 +106,7 @@ builtin_extensions = ( # 1st party extensions 'sphinxcontrib.applehelp', 'sphinxcontrib.devhelp', + 'sphinxcontrib.htmlhelp', 'sphinxcontrib.qthelp', # Strictly, alabaster theme is not a builtin extension, # but it is loaded automatically to use it as default theme. diff --git a/sphinx/builders/htmlhelp.py b/sphinx/builders/htmlhelp.py index ce30affba..2e7e8f083 100644 --- a/sphinx/builders/htmlhelp.py +++ b/sphinx/builders/htmlhelp.py @@ -9,374 +9,36 @@ :license: BSD, see LICENSE for details. """ -import html -import os import warnings -from os import path -from docutils import nodes +from sphinxcontrib.htmlhelp import ( + chm_locales, chm_htmlescape, HTMLHelpBuilder, default_htmlhelp_basename +) + +from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias -from sphinx import addnodes -from sphinx import package_dir -from sphinx.builders.html import StandaloneHTMLBuilder -from sphinx.deprecation import RemovedInSphinx40Warning -from sphinx.environment.adapters.indexentries import IndexEntries -from sphinx.locale import __ -from sphinx.util import logging -from sphinx.util import progress_message -from sphinx.util.fileutil import copy_asset_file -from sphinx.util.nodes import NodeMatcher -from sphinx.util.osutil import make_filename_from_project, relpath -from sphinx.util.template import SphinxRenderer if False: # For type annotation - from typing import Any, Dict, IO, List, Match, Tuple # NOQA + from typing import Any, Dict # NOQA from sphinx.application import Sphinx # NOQA - from sphinx.config import Config # NOQA -logger = logging.getLogger(__name__) - -template_dir = path.join(package_dir, 'templates', 'htmlhelp') - - -# Project file (*.hhp) template. 'outname' is the file basename (like -# the pythlp in pythlp.hhp); 'version' is the doc version number (like -# the 2.2 in Python 2.2). -# The magical numbers in the long line under [WINDOWS] set most of the -# user-visible features (visible buttons, tabs, etc). -# About 0x10384e: This defines the buttons in the help viewer. The -# following defns are taken from htmlhelp.h. Not all possibilities -# actually work, and not all those that work are available from the Help -# Workshop GUI. In particular, the Zoom/Font button works and is not -# available from the GUI. The ones we're using are marked with 'x': -# -# 0x000002 Hide/Show x -# 0x000004 Back x -# 0x000008 Forward x -# 0x000010 Stop -# 0x000020 Refresh -# 0x000040 Home x -# 0x000080 Forward -# 0x000100 Back -# 0x000200 Notes -# 0x000400 Contents -# 0x000800 Locate x -# 0x001000 Options x -# 0x002000 Print x -# 0x004000 Index -# 0x008000 Search -# 0x010000 History -# 0x020000 Favorites -# 0x040000 Jump 1 -# 0x080000 Jump 2 -# 0x100000 Zoom/Font x -# 0x200000 TOC Next -# 0x400000 TOC Prev - -object_sitemap = '''\ - - - - -''' - -# The following list includes only languages supported by Sphinx. See -# https://docs.microsoft.com/en-us/previous-versions/windows/embedded/ms930130(v=msdn.10) -# for more. -chm_locales = { - # lang: LCID, encoding - 'ca': (0x403, 'cp1252'), - 'cs': (0x405, 'cp1250'), - 'da': (0x406, 'cp1252'), - 'de': (0x407, 'cp1252'), - 'en': (0x409, 'cp1252'), - 'es': (0x40a, 'cp1252'), - 'et': (0x425, 'cp1257'), - 'fa': (0x429, 'cp1256'), - 'fi': (0x40b, 'cp1252'), - 'fr': (0x40c, 'cp1252'), - 'hr': (0x41a, 'cp1250'), - 'hu': (0x40e, 'cp1250'), - 'it': (0x410, 'cp1252'), - 'ja': (0x411, 'cp932'), - 'ko': (0x412, 'cp949'), - 'lt': (0x427, 'cp1257'), - 'lv': (0x426, 'cp1257'), - 'nl': (0x413, 'cp1252'), - 'no_NB': (0x414, 'cp1252'), - 'pl': (0x415, 'cp1250'), - 'pt_BR': (0x416, 'cp1252'), - 'ru': (0x419, 'cp1251'), - 'sk': (0x41b, 'cp1250'), - 'sl': (0x424, 'cp1250'), - 'sv': (0x41d, 'cp1252'), - 'tr': (0x41f, 'cp1254'), - 'uk_UA': (0x422, 'cp1251'), - 'zh_CN': (0x804, 'cp936'), - 'zh_TW': (0x404, 'cp950'), -} - - -def chm_htmlescape(s, quote=True): - # type: (str, bool) -> str - """ - chm_htmlescape() is a wrapper of html.escape(). - .hhc/.hhk files don't recognize hex escaping, we need convert - hex escaping to decimal escaping. for example: ``'`` -> ``'`` - html.escape() may generates a hex escaping ``'`` for single - quote ``'``, this wrapper fixes this. - """ - s = html.escape(s, quote) - s = s.replace(''', ''') # re-escape as decimal - return s - - -class ToCTreeVisitor(nodes.NodeVisitor): - def __init__(self, document): - # type: (nodes.document) -> None - super().__init__(document) - self.body = [] # type: List[str] - self.depth = 0 - - def append(self, text): - # type: (str) -> None - indent = ' ' * (self.depth - 1) - self.body.append(indent + text) - - def astext(self): - # type: () -> str - return '\n'.join(self.body) - - def unknown_visit(self, node): - # type: (nodes.Node) -> None - pass - - def unknown_departure(self, node): - # type: (nodes.Node) -> None - pass - - def visit_bullet_list(self, node): - # type: (nodes.Element) -> None - if self.depth > 0: - self.append('') - - def visit_list_item(self, node): - # type: (nodes.Element) -> None - self.append('
  • ') - self.depth += 1 - - def depart_list_item(self, node): - # type: (nodes.Element) -> None - self.depth -= 1 - self.append('
  • ') - - def visit_reference(self, node): - # type: (nodes.Element) -> None - title = chm_htmlescape(node.astext(), True) - self.append('') - self.append(' ' % title) - self.append(' ' % node['refuri']) - self.append('') - raise nodes.SkipNode - - -class HTMLHelpBuilder(StandaloneHTMLBuilder): - """ - Builder that also outputs Windows HTML help project, contents and - index files. Adapted from the original Doc/tools/prechm.py. - """ - name = 'htmlhelp' - epilog = __('You can now run HTML Help Workshop with the .htp file in ' - '%(outdir)s.') - - # don't copy the reST source - copysource = False - supported_image_types = ['image/png', 'image/gif', 'image/jpeg'] - - # don't add links - add_permalinks = False - # don't add sidebar etc. - embedded = True - - # don't generate search index or include search page - search = False - - lcid = 0x409 - encoding = 'cp1252' - - def init(self): - # type: () -> None - # the output files for HTML help is .html by default - self.out_suffix = '.html' - self.link_suffix = '.html' - super().init() - # determine the correct locale setting - locale = chm_locales.get(self.config.language) - if locale is not None: - self.lcid, self.encoding = locale - - def open_file(self, outdir, basename, mode='w'): - # type: (str, str, str) -> IO - # open a file with the correct encoding for the selected language - warnings.warn('HTMLHelpBuilder.open_file() is deprecated.', - RemovedInSphinx40Warning) - return open(path.join(outdir, basename), mode, encoding=self.encoding, - errors='xmlcharrefreplace') - - def update_page_context(self, pagename, templatename, ctx, event_arg): - # type: (str, str, Dict, str) -> None - ctx['encoding'] = self.encoding - - def handle_finish(self): - # type: () -> None - self.copy_stopword_list() - self.build_project_file() - self.build_toc_file() - self.build_hhx(self.outdir, self.config.htmlhelp_basename) - - def write_doc(self, docname, doctree): - # type: (str, nodes.document) -> None - for node in doctree.traverse(nodes.reference): - # add ``target=_blank`` attributes to external links - if node.get('internal') is None and 'refuri' in node: - node['target'] = '_blank' - - super().write_doc(docname, doctree) - - def render(self, name, context): - # type: (str, Dict) -> str - template = SphinxRenderer(template_dir) - return template.render(name, context) - - @progress_message(__('copying stopword list')) - def copy_stopword_list(self): - # type: () -> None - """Copy a stopword list (.stp) to outdir. - - The stopword list contains a list of words the full text search facility - shouldn't index. Note that this list must be pretty small. Different - versions of the MS docs claim the file has a maximum size of 256 or 512 - bytes (including \r\n at the end of each line). Note that "and", "or", - "not" and "near" are operators in the search language, so no point - indexing them even if we wanted to. - """ - template = path.join(template_dir, 'project.stp') - filename = path.join(self.outdir, self.config.htmlhelp_basename + '.stp') - copy_asset_file(template, filename) - - @progress_message(__('writing project file')) - def build_project_file(self): - # type: () -> None - """Create a project file (.hhp) on outdir.""" - # scan project files - project_files = [] # type: List[str] - for root, dirs, files in os.walk(self.outdir): - dirs.sort() - files.sort() - in_staticdir = root.startswith(path.join(self.outdir, '_static')) - for fn in sorted(files): - if (in_staticdir and not fn.endswith('.js')) or fn.endswith('.html'): - fn = relpath(path.join(root, fn), self.outdir) - project_files.append(fn.replace(os.sep, '\\')) - - filename = path.join(self.outdir, self.config.htmlhelp_basename + '.hhp') - with open(filename, 'w', encoding=self.encoding, errors='xmlcharrefreplace') as f: - context = { - 'outname': self.config.htmlhelp_basename, - 'title': self.config.html_title, - 'version': self.config.version, - 'project': self.config.project, - 'lcid': self.lcid, - 'master_doc': self.config.master_doc + self.out_suffix, - 'files': project_files, - } - body = self.render('project.hhp', context) - f.write(body) - - @progress_message(__('writing TOC file')) - def build_toc_file(self): - # type: () -> None - """Create a ToC file (.hhp) on outdir.""" - filename = path.join(self.outdir, self.config.htmlhelp_basename + '.hhc') - with open(filename, 'w', encoding=self.encoding, errors='xmlcharrefreplace') as f: - toctree = self.env.get_and_resolve_doctree(self.config.master_doc, self, - prune_toctrees=False) - visitor = ToCTreeVisitor(toctree) - matcher = NodeMatcher(addnodes.compact_paragraph, toctree=True) - for node in toctree.traverse(matcher): # type: addnodes.compact_paragraph - node.walkabout(visitor) - - context = { - 'body': visitor.astext(), - 'suffix': self.out_suffix, - 'short_title': self.config.html_short_title, - 'master_doc': self.config.master_doc, - 'domain_indices': self.domain_indices, - } - f.write(self.render('project.hhc', context)) - - def build_hhx(self, outdir, outname): - # type: (str, str) -> None - logger.info(__('writing index file...')) - index = IndexEntries(self.env).create_index(self) - filename = path.join(outdir, outname + '.hhk') - with open(filename, 'w', encoding=self.encoding, errors='xmlcharrefreplace') as f: - f.write('\n') - - -def default_htmlhelp_basename(config): - # type: (Config) -> str - """Better default htmlhelp_basename setting.""" - return make_filename_from_project(config.project) + 'doc' +deprecated_alias('sphinx.builders.devhelp', + { + 'chm_locales': chm_locales, + 'chm_htmlescape': chm_htmlescape, + 'HTMLHelpBuilder': HTMLHelpBuilder, + 'default_htmlhelp_basename': default_htmlhelp_basename, + }, + RemovedInSphinx40Warning) def setup(app): # type: (Sphinx) -> Dict[str, Any] - app.setup_extension('sphinx.builders.html') - app.add_builder(HTMLHelpBuilder) - - app.add_config_value('htmlhelp_basename', default_htmlhelp_basename, None) - app.add_config_value('htmlhelp_file_suffix', None, 'html', [str]) - app.add_config_value('htmlhelp_link_suffix', None, 'html', [str]) + warnings.warn('sphinx.builders.htmlhelp has been moved to sphinxcontrib-htmlhelp.', + RemovedInSphinx40Warning) + app.setup_extension('sphinxcontrib.htmlhelp') return { 'version': 'builtin', diff --git a/tests/roots/test-build-htmlhelp/conf.py b/tests/roots/test-build-htmlhelp/conf.py deleted file mode 100644 index 9b6a55417..000000000 --- a/tests/roots/test-build-htmlhelp/conf.py +++ /dev/null @@ -1 +0,0 @@ -project = 'test' diff --git a/tests/roots/test-build-htmlhelp/index.rst b/tests/roots/test-build-htmlhelp/index.rst deleted file mode 100644 index 2d9e563d6..000000000 --- a/tests/roots/test-build-htmlhelp/index.rst +++ /dev/null @@ -1,19 +0,0 @@ -Index markup ------------- - -.. index:: - single: entry - pair: entry; pair - double: entry; double - triple: index; entry; triple - keyword: with - see: from; to - seealso: fromalso; toalso - -.. index:: - !Main, !Other - !single: entry; pair - -.. index:: triple-quoted string, Unicode Consortium, raw string - single: """; string literal - single: '''; string literal diff --git a/tests/roots/test-htmlhelp-hhc/bar.rst b/tests/roots/test-htmlhelp-hhc/bar.rst deleted file mode 100644 index 0d1785bcf..000000000 --- a/tests/roots/test-htmlhelp-hhc/bar.rst +++ /dev/null @@ -1,2 +0,0 @@ -bar ---- diff --git a/tests/roots/test-htmlhelp-hhc/baz.rst b/tests/roots/test-htmlhelp-hhc/baz.rst deleted file mode 100644 index 69fe26493..000000000 --- a/tests/roots/test-htmlhelp-hhc/baz.rst +++ /dev/null @@ -1,2 +0,0 @@ -baz ---- diff --git a/tests/roots/test-htmlhelp-hhc/conf.py b/tests/roots/test-htmlhelp-hhc/conf.py deleted file mode 100644 index 20447e040..000000000 --- a/tests/roots/test-htmlhelp-hhc/conf.py +++ /dev/null @@ -1 +0,0 @@ -html_short_title = "Sphinx's documentation" diff --git a/tests/roots/test-htmlhelp-hhc/foo.rst b/tests/roots/test-htmlhelp-hhc/foo.rst deleted file mode 100644 index 6e06b7e69..000000000 --- a/tests/roots/test-htmlhelp-hhc/foo.rst +++ /dev/null @@ -1,6 +0,0 @@ -foo ---- - -.. toctree:: - - bar diff --git a/tests/roots/test-htmlhelp-hhc/index.rst b/tests/roots/test-htmlhelp-hhc/index.rst deleted file mode 100644 index 9b43b4129..000000000 --- a/tests/roots/test-htmlhelp-hhc/index.rst +++ /dev/null @@ -1,15 +0,0 @@ -test-htmlhelp-domain_indices ----------------------------- - -section -~~~~~~~ - -.. py:module:: sphinx - -subsection -^^^^^^^^^^ - -.. toctree:: - - foo - baz diff --git a/tests/test_build.py b/tests/test_build.py index 61fb2fed6..b25254529 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -60,7 +60,7 @@ def nonascii_srcdir(request, rootdir, sphinx_test_tempdir): "buildername", [ # note: no 'html' - if it's ok with dirhtml it's ok with html - 'dirhtml', 'singlehtml', 'pickle', 'json', 'text', 'htmlhelp', + 'dirhtml', 'singlehtml', 'pickle', 'json', 'text', 'changes', 'xml', 'pseudoxml', 'linkcheck', ], ) diff --git a/tests/test_build_htmlhelp.py b/tests/test_build_htmlhelp.py deleted file mode 100644 index 4ad244a4e..000000000 --- a/tests/test_build_htmlhelp.py +++ /dev/null @@ -1,131 +0,0 @@ -""" - test_build_htmlhelp - ~~~~~~~~~~~~~~~~~~~ - - Test the HTML Help builder and check output against XPath. - - :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -""" - -import re - -import pytest -from html5lib import HTMLParser - -from sphinx.builders.htmlhelp import chm_htmlescape, default_htmlhelp_basename -from sphinx.config import Config - - -@pytest.mark.sphinx('htmlhelp', testroot='basic') -def test_build_htmlhelp(app, status, warning): - app.build() - - hhp = (app.outdir / 'pythondoc.hhp').text() - assert 'Compiled file=pythondoc.chm' in hhp - assert 'Contents file=pythondoc.hhc' in hhp - assert 'Default Window=pythondoc' in hhp - assert 'Default topic=index.html' in hhp - assert 'Full text search stop list file=pythondoc.stp' in hhp - assert 'Index file=pythondoc.hhk' in hhp - assert 'Language=0x409' in hhp - assert 'Title=Python documentation' in hhp - assert ('pythondoc="Python documentation","pythondoc.hhc",' - '"pythondoc.hhk","index.html","index.html",,,,,' - '0x63520,220,0x10384e,[0,0,1024,768],,,,,,,0' in hhp) - - files = ['genindex.html', 'index.html', '_static\\alabaster.css', '_static\\basic.css', - '_static\\custom.css', '_static\\file.png', '_static\\minus.png', - '_static\\plus.png', '_static\\pygments.css'] - assert '[FILES]\n%s' % '\n'.join(files) in hhp - - -@pytest.mark.sphinx('htmlhelp', testroot='basic') -def test_default_htmlhelp_file_suffix(app, warning): - assert app.builder.out_suffix == '.html' - - -@pytest.mark.sphinx('htmlhelp', testroot='basic', - confoverrides={'htmlhelp_file_suffix': '.htm'}) -def test_htmlhelp_file_suffix(app, warning): - assert app.builder.out_suffix == '.htm' - - -def test_default_htmlhelp_basename(): - config = Config({'project': 'Sphinx Documentation'}) - config.init_values() - assert default_htmlhelp_basename(config) == 'sphinxdoc' - - -@pytest.mark.sphinx('htmlhelp', testroot='build-htmlhelp') -def test_chm(app): - app.build() - - # check .hhk file - outname = app.builder.config.htmlhelp_basename - hhk_path = str(app.outdir / outname + '.hhk') - - with open(hhk_path, 'rb') as f: - data = f.read() - m = re.search(br'&#[xX][0-9a-fA-F]+;', data) - assert m is None, 'Hex escaping exists in .hhk file: ' + str(m.group(0)) - - -@pytest.mark.sphinx('htmlhelp', testroot='htmlhelp-hhc') -def test_htmlhelp_hhc(app): - app.build() - - def assert_sitemap(node, name, filename): - assert node.tag == 'object' - assert len(node) == 2 - assert node[0].tag == 'param' - assert node[0].attrib == {'name': 'Name', 'value': name} - assert node[1].tag == 'param' - assert node[1].attrib == {'name': 'Local', 'value': filename} - - # .hhc file - hhc = (app.outdir / 'pythondoc.hhc').text() - tree = HTMLParser(namespaceHTMLElements=False).parse(hhc) - items = tree.find('.//body/ul') - assert len(items) == 4 - - # index - assert items[0].tag == 'li' - assert len(items[0]) == 1 - assert_sitemap(items[0][0], "Sphinx's documentation", 'index.html') - - # py-modindex - assert items[1].tag == 'li' - assert len(items[1]) == 1 - assert_sitemap(items[1][0], 'Python Module Index', 'py-modindex.html') - - # toctree - assert items[2].tag == 'li' - assert len(items[2]) == 2 - assert_sitemap(items[2][0], 'foo', 'foo.html') - - assert items[2][1].tag == 'ul' - assert len(items[2][1]) == 1 - assert items[2][1][0].tag == 'li' - assert_sitemap(items[2][1][0][0], 'bar', 'bar.html') - - assert items[3].tag == 'li' - assert len(items[3]) == 1 - assert_sitemap(items[3][0], 'baz', 'baz.html') - - # single quotes should be escaped as decimal (') - assert "Sphinx's documentation" in hhc - - -def test_chm_htmlescape(): - assert chm_htmlescape('Hello world') == 'Hello world' - assert chm_htmlescape(u'Unicode 文字') == u'Unicode 文字' - assert chm_htmlescape('E') == '&#x45' - - assert chm_htmlescape(' "world"') == '<Hello> "world"' - assert chm_htmlescape(' "world"', True) == '<Hello> "world"' - assert chm_htmlescape(' "world"', False) == '<Hello> "world"' - - assert chm_htmlescape("Hello 'world'") == "Hello 'world'" - assert chm_htmlescape("Hello 'world'", True) == "Hello 'world'" - assert chm_htmlescape("Hello 'world'", False) == "Hello 'world'"