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('
')
-
- self.depth += 1
-
- def depart_bullet_list(self, node):
- # type: (nodes.Element) -> None
- self.depth -= 1
- 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('')
- 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 write_index(title, refs, subitems):
- # type: (str, List[Tuple[str, str]], List[Tuple[str, List[Tuple[str, str]]]]) -> None # NOQA
- def write_param(name, value):
- # type: (str, str) -> None
- item = ' \n' % (name, value)
- f.write(item)
- title = chm_htmlescape(title, True)
- f.write('- \n')
- if subitems:
- f.write('
')
- for subitem in subitems:
- write_index(subitem[0], subitem[1], [])
- f.write('
')
- for (key, group) in index:
- for title, (refs, subitems, key_) in group:
- write_index(title, refs, subitems)
- 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') == 'E'
-
- 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'"