diff --git a/sphinx/builders/htmlhelp.py b/sphinx/builders/htmlhelp.py
index 4a3ca98c4..a182c7632 100644
--- a/sphinx/builders/htmlhelp.py
+++ b/sphinx/builders/htmlhelp.py
@@ -26,7 +26,8 @@ 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
+from sphinx.util.osutil import make_filename_from_project, relpath
+from sphinx.util.template import SphinxRenderer
if False:
# For type annotation
@@ -74,28 +75,6 @@ template_dir = path.join(package_dir, 'templates', 'htmlhelp')
# 0x200000 TOC Next
# 0x400000 TOC Prev
-project_template = '''\
-[OPTIONS]
-Binary TOC=No
-Binary Index=No
-Compiled file=%(outname)s.chm
-Contents file=%(outname)s.hhc
-Default Window=%(outname)s
-Default topic=%(master_doc)s
-Display compile progress=No
-Full text search stop list file=%(outname)s.stp
-Full-text search=Yes
-Index file=%(outname)s.hhk
-Language=%(lcid)#x
-Title=%(title)s
-
-[WINDOWS]
-%(outname)s="%(title)s","%(outname)s.hhc","%(outname)s.hhk",\
-"%(master_doc)s","%(master_doc)s",,,,,0x63520,220,0x10384e,[0,0,1024,768],,,,,,,0
-
-[FILES]
-'''
-
contents_header = '''\
@@ -222,6 +201,7 @@ class HTMLHelpBuilder(StandaloneHTMLBuilder):
def handle_finish(self):
# type: () -> None
self.copy_stopword_list()
+ self.build_project_file()
self.build_hhx(self.outdir, self.config.htmlhelp_basename)
def write_doc(self, docname, doctree):
@@ -233,6 +213,11 @@ class HTMLHelpBuilder(StandaloneHTMLBuilder):
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
@@ -249,32 +234,37 @@ class HTMLHelpBuilder(StandaloneHTMLBuilder):
filename = path.join(self.outdir, self.config.htmlhelp_basename + '.stp')
copy_asset_file(template, filename)
- def build_hhx(self, outdir, outname):
- # type: (str, str) -> None
- logger.info(__('writing project file...'))
- filename = path.join(outdir, outname + '.hhp')
+ @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:
- f.write(project_template % {
- 'outname': outname,
+ 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
- })
- if not outdir.endswith(os.sep):
- outdir += os.sep
- olen = len(outdir)
- for root, dirs, files in os.walk(outdir):
- dirs.sort()
- files.sort()
- staticdir = root.startswith(path.join(outdir, '_static'))
- for fn in sorted(files):
- if (staticdir and not fn.endswith('.js')) or \
- fn.endswith('.html'):
- print(path.join(root, fn)[olen:].replace(os.sep, '\\'),
- file=f)
+ 'master_doc': self.config.master_doc + self.out_suffix,
+ 'files': project_files,
+ }
+ body = self.render('project.hhp', context)
+ f.write(body)
+ def build_hhx(self, outdir, outname):
+ # type: (str, str) -> None
logger.info(__('writing TOC file...'))
filename = path.join(outdir, outname + '.hhc')
with open(filename, 'w', encoding=self.encoding, errors='xmlcharrefreplace') as f:
diff --git a/sphinx/templates/htmlhelp/project.hhp b/sphinx/templates/htmlhelp/project.hhp
new file mode 100644
index 000000000..b647b3713
--- /dev/null
+++ b/sphinx/templates/htmlhelp/project.hhp
@@ -0,0 +1,21 @@
+[OPTIONS]
+Binary TOC=No
+Binary Index=No
+Compiled file={{ outname }}.chm
+Contents file={{ outname }}.hhc
+Default Window={{ outname }}
+Default topic={{ master_doc }}
+Display compile progress=No
+Full text search stop list file={{ outname }}.stp
+Full-text search=Yes
+Index file={{ outname }}.hhk
+Language={{ "%#x"|format(lcid) }}
+Title={{ title }}
+
+[WINDOWS]
+{{ outname }}="{{ title }}","{{ outname }}.hhc","{{ outname }}.hhk","{{ master_doc }}","{{ master_doc }}",,,,,0x63520,220,0x10384e,[0,0,1024,768],,,,,,,0
+
+[FILES]
+{%- for filename in files %}
+{{ filename }}
+{%- endfor %}
diff --git a/tests/test_build_htmlhelp.py b/tests/test_build_htmlhelp.py
index 980a565e5..18acca921 100644
--- a/tests/test_build_htmlhelp.py
+++ b/tests/test_build_htmlhelp.py
@@ -18,6 +18,29 @@ from sphinx.builders.htmlhelp import 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'