refactor: htmlhelp: Generate .hhc file from template

This commit is contained in:
Takeshi KOMIYA 2019-01-26 17:45:07 +09:00
parent 28b0b744b6
commit 7415f64eab
8 changed files with 185 additions and 61 deletions

View File

@ -75,24 +75,6 @@ template_dir = path.join(package_dir, 'templates', 'htmlhelp')
# 0x200000 TOC Next # 0x200000 TOC Next
# 0x400000 TOC Prev # 0x400000 TOC Prev
contents_header = '''\
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<HTML>
<HEAD>
<meta name="GENERATOR" content="Microsoft&reg; HTML Help Workshop 4.1">
<!-- Sitemap 1.0 -->
</HEAD><BODY>
<OBJECT type="text/site properties">
<param name="Window Styles" value="0x801227">
<param name="ImageType" value="Folder">
</OBJECT>
<UL>
'''
contents_footer = '''\
</UL></BODY></HTML>
'''
object_sitemap = '''\ object_sitemap = '''\
<OBJECT type="text/sitemap"> <OBJECT type="text/sitemap">
<param name="Name" value="%s"> <param name="Name" value="%s">
@ -151,6 +133,63 @@ def chm_htmlescape(s, quote=True):
return s 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('<UL>')
self.depth += 1
def depart_bullet_list(self, node):
# type: (nodes.Element) -> None
self.depth -= 1
if self.depth > 0:
self.append('</UL>')
def visit_list_item(self, node):
# type: (nodes.Element) -> None
self.append('<LI>')
self.depth += 1
def depart_list_item(self, node):
# type: (nodes.Element) -> None
self.depth -= 1
self.append('</LI>')
def visit_reference(self, node):
# type: (nodes.Element) -> None
title = chm_htmlescape(node.astext(), True)
self.append('<OBJECT type="text/sitemap">')
self.append(' <PARAM name="Name" value="%s" />' % title)
self.append(' <PARAM name="Local" value="%s" />' % node['refuri'])
self.append('</OBJECT>')
raise nodes.SkipNode
class HTMLHelpBuilder(StandaloneHTMLBuilder): class HTMLHelpBuilder(StandaloneHTMLBuilder):
""" """
Builder that also outputs Windows HTML help project, contents and Builder that also outputs Windows HTML help project, contents and
@ -202,6 +241,7 @@ class HTMLHelpBuilder(StandaloneHTMLBuilder):
# type: () -> None # type: () -> None
self.copy_stopword_list() self.copy_stopword_list()
self.build_project_file() self.build_project_file()
self.build_toc_file()
self.build_hhx(self.outdir, self.config.htmlhelp_basename) self.build_hhx(self.outdir, self.config.htmlhelp_basename)
def write_doc(self, docname, doctree): def write_doc(self, docname, doctree):
@ -263,48 +303,30 @@ class HTMLHelpBuilder(StandaloneHTMLBuilder):
body = self.render('project.hhp', context) body = self.render('project.hhp', context)
f.write(body) 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): def build_hhx(self, outdir, outname):
# type: (str, str) -> None # 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:
f.write(contents_header)
# special books
f.write('<LI> ' + object_sitemap % (self.config.html_short_title,
self.config.master_doc + self.out_suffix))
for indexname, indexcls, content, collapse in self.domain_indices:
f.write('<LI> ' + object_sitemap % (indexcls.localname,
'%s.html' % indexname))
# the TOC
tocdoc = self.env.get_and_resolve_doctree(
self.config.master_doc, self, prune_toctrees=False)
def write_toc(node, ullevel=0):
# type: (nodes.Node, int) -> None
if isinstance(node, nodes.list_item):
f.write('<LI> ')
for subnode in node:
write_toc(subnode, ullevel)
elif isinstance(node, nodes.reference):
link = node['refuri']
title = chm_htmlescape(node.astext(), True)
f.write(object_sitemap % (title, link))
elif isinstance(node, nodes.bullet_list):
if ullevel != 0:
f.write('<UL>\n')
for subnode in node:
write_toc(subnode, ullevel + 1)
if ullevel != 0:
f.write('</UL>\n')
elif isinstance(node, addnodes.compact_paragraph):
for subnode in node:
write_toc(subnode, ullevel)
matcher = NodeMatcher(addnodes.compact_paragraph, toctree=True)
for node in tocdoc.traverse(matcher): # type: addnodes.compact_paragraph
write_toc(node)
f.write(contents_footer)
logger.info(__('writing index file...')) logger.info(__('writing index file...'))
index = IndexEntries(self.env).create_index(self) index = IndexEntries(self.env).create_index(self)
filename = path.join(outdir, outname + '.hhk') filename = path.join(outdir, outname + '.hhk')

View File

@ -0,0 +1,31 @@
{%- macro sitemap(name, docname) -%}
<OBJECT type="text/sitemap">
<PARAM name="Name" value="{{ name|e }}" />
<PARAM name="Local" value="{{ docname|e }}{{ suffix }}" />
</OBJECT>
{%- endmacro -%}
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<HTML>
<HEAD>
<META name="GENERATOR" content="Microsoft&reg; HTML Help Workshop 4.1" />
<!-- Sitemap 1.0 -->
</HEAD>
<BODY>
<OBJECT type="text/site properties">
<PARAM name="Window Styles" value="0x801227" />
<PARAM name="ImageType" value="Folder" />
</OBJECT>
<UL>
<LI>
{{ sitemap(short_title, master_doc)|indent(8) }}
</LI>
{%- for indexname, indexcls, content, collapse in domain_indices %}
<LI>
{{ sitemap(indexcls.localname, indexname)|indent(8) }}
</LI>
{%- endfor %}
{{ body|indent(6) }}
</UL>
</BODY>
</HTML>

View File

@ -0,0 +1,2 @@
bar
---

View File

@ -0,0 +1,2 @@
baz
---

View File

@ -0,0 +1 @@
html_short_title = "Sphinx's documentation"

View File

@ -0,0 +1,6 @@
foo
---
.. toctree::
bar

View File

@ -0,0 +1,15 @@
test-htmlhelp-domain_indices
----------------------------
section
~~~~~~~
.. py:module:: sphinx
subsection
^^^^^^^^^^
.. toctree::
foo
baz

View File

@ -11,10 +11,9 @@
import re import re
import pytest import pytest
from html5lib import HTMLParser
from sphinx.builders.htmlhelp import chm_htmlescape from sphinx.builders.htmlhelp import chm_htmlescape, default_htmlhelp_basename
from sphinx.builders.htmlhelp import default_htmlhelp_basename
from sphinx.config import Config from sphinx.config import Config
@ -72,6 +71,52 @@ def test_chm(app):
assert m is None, 'Hex escaping exists in .hhk file: ' + str(m.group(0)) 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 (&#39;)
assert "Sphinx&#39;s documentation" in hhc
def test_chm_htmlescape(): def test_chm_htmlescape():
assert chm_htmlescape('Hello world') == 'Hello world' assert chm_htmlescape('Hello world') == 'Hello world'
assert chm_htmlescape(u'Unicode 文字') == u'Unicode 文字' assert chm_htmlescape(u'Unicode 文字') == u'Unicode 文字'