Merge pull request #5968 from tk0miya/separate_qthelp

Separate qthelp to sphinxcontrib package
This commit is contained in:
Takeshi KOMIYA
2019-01-20 00:28:08 +09:00
committed by GitHub
10 changed files with 30 additions and 419 deletions
+1
View File
@@ -22,6 +22,7 @@ Dependencies
* Some packages are separated to sub packages:
- sphinxcontrib.jsmath
- sphinxcontrib.qthelp
Incompatible changes
--------------------
+5
View File
@@ -290,6 +290,11 @@ The following is a list of deprecated interfaces.
- 4.0
- ``open()``
* - ``sphinx.builders.qthelp``
- 2.0
- 4.0
- ``sphinxcontrib.qthelp``
* - ``sphinx.cmd.quickstart.term_decode()``
- 2.0
- 4.0
+5 -1
View File
@@ -72,13 +72,17 @@ The builder's "name" must be given to the **-b** command-line option of
.. autoattribute:: supported_image_types
.. module:: sphinx.builders.qthelp
.. module:: sphinxcontrib.qthelp
.. class:: QtHelpBuilder
This builder produces the same output as the standalone HTML builder, but
also generates `Qt help`_ collection support files that allow the Qt
collection generator to compile them.
.. versionchanged:: 2.0
Moved to sphinxcontrib.qthelp from sphinx.builders package.
.. autoattribute:: name
.. autoattribute:: format
+1
View File
@@ -16,6 +16,7 @@ if sys.version_info < (3, 5):
install_requires = [
'sphinxcontrib-jsmath',
'sphinxcontrib-qthelp',
'Jinja2>=2.3',
'Pygments>=2.0',
'docutils>=0.12',
+2 -1
View File
@@ -73,7 +73,6 @@ builtin_extensions = (
'sphinx.builders.latex',
'sphinx.builders.linkcheck',
'sphinx.builders.manpage',
'sphinx.builders.qthelp',
'sphinx.builders.singlehtml',
'sphinx.builders.texinfo',
'sphinx.builders.text',
@@ -107,6 +106,8 @@ builtin_extensions = (
'sphinx.environment.collectors.title',
'sphinx.environment.collectors.toctree',
'sphinx.environment.collectors.indexentries',
# 1st party extensions
'sphinxcontrib.qthelp',
# Strictly, alabaster theme is not a builtin extension,
# but it is loaded automatically to use it as default theme.
'alabaster',
+15 -254
View File
@@ -8,275 +8,36 @@
:license: BSD, see LICENSE for details.
"""
import html
import os
import posixpath
import re
from os import path
from typing import Iterable, cast
import warnings
from docutils import nodes
from sphinxcontrib.qthelp import QtHelpBuilder, render_file
from sphinx import addnodes
from sphinx import package_dir
from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.environment.adapters.indexentries import IndexEntries
from sphinx.locale import __
from sphinx.util import logging
from sphinx.util.nodes import NodeMatcher
from sphinx.util.osutil import canon_path, make_filename
from sphinx.util.template import SphinxRenderer
import sphinx
from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias
if False:
# For type annotation
from typing import Any, Dict, List, Tuple # NOQA
from typing import Any, Dict # NOQA
from sphinx.application import Sphinx # NOQA
logger = logging.getLogger(__name__)
_idpattern = re.compile(
r'(?P<title>.+) (\((class in )?(?P<id>[\w\.]+)( (?P<descr>\w+))?\))$')
section_template = '<section title="%(title)s" ref="%(ref)s"/>'
def render_file(filename, **kwargs):
# type: (str, Any) -> str
pathname = os.path.join(package_dir, 'templates', 'qthelp', filename)
return SphinxRenderer.render_from_file(pathname, kwargs)
class QtHelpBuilder(StandaloneHTMLBuilder):
"""
Builder that also outputs Qt help project, contents and index files.
"""
name = 'qthelp'
epilog = __('You can now run "qcollectiongenerator" with the .qhcp '
'project file in %(outdir)s, like this:\n'
'$ qcollectiongenerator %(outdir)s/%(project)s.qhcp\n'
'To view the help file:\n'
'$ assistant -collectionFile %(outdir)s/%(project)s.qhc')
# don't copy the reST source
copysource = False
supported_image_types = ['image/svg+xml', 'image/png', 'image/gif',
'image/jpeg']
# don't add links
add_permalinks = False
# don't add sidebar etc.
embedded = True
# disable download role
download_support = False
# don't generate the search index or include the search page
search = False
def init(self):
# type: () -> None
super().init()
# the output files for HTML help must be .html only
self.out_suffix = '.html'
self.link_suffix = '.html'
# self.config.html_style = 'traditional.css'
def get_theme_config(self):
# type: () -> Tuple[str, Dict]
return self.config.qthelp_theme, self.config.qthelp_theme_options
def handle_finish(self):
# type: () -> None
self.build_qhp(self.outdir, self.config.qthelp_basename)
def build_qhp(self, outdir, outname):
# type: (str, str) -> None
logger.info(__('writing project file...'))
# sections
tocdoc = self.env.get_and_resolve_doctree(self.config.master_doc, self,
prune_toctrees=False)
sections = []
matcher = NodeMatcher(addnodes.compact_paragraph, toctree=True)
for node in tocdoc.traverse(matcher): # type: addnodes.compact_paragraph
sections.extend(self.write_toc(node))
for indexname, indexcls, content, collapse in self.domain_indices:
item = section_template % {'title': indexcls.localname,
'ref': '%s.html' % indexname}
sections.append(' ' * 4 * 4 + item)
sections = '\n'.join(sections) # type: ignore
# keywords
keywords = []
index = IndexEntries(self.env).create_index(self, group_entries=False)
for (key, group) in index:
for title, (refs, subitems, key_) in group:
keywords.extend(self.build_keywords(title, refs, subitems))
keywords = '\n'.join(keywords) # type: ignore
# it seems that the "namespace" may not contain non-alphanumeric
# characters, and more than one successive dot, or leading/trailing
# dots, are also forbidden
if self.config.qthelp_namespace:
nspace = self.config.qthelp_namespace
else:
nspace = 'org.sphinx.%s.%s' % (outname, self.config.version)
nspace = re.sub(r'[^a-zA-Z0-9.\-]', '', nspace)
nspace = re.sub(r'\.+', '.', nspace).strip('.')
nspace = nspace.lower()
# write the project file
with open(path.join(outdir, outname + '.qhp'), 'w', encoding='utf-8') as f:
body = render_file('project.qhp', outname=outname,
title=self.config.html_title, version=self.config.version,
project=self.config.project, namespace=nspace,
master_doc=self.config.master_doc,
sections=sections, keywords=keywords,
files=self.get_project_files(outdir))
f.write(body)
homepage = 'qthelp://' + posixpath.join(
nspace, 'doc', self.get_target_uri(self.config.master_doc))
startpage = 'qthelp://' + posixpath.join(nspace, 'doc', 'index.html')
logger.info(__('writing collection project file...'))
with open(path.join(outdir, outname + '.qhcp'), 'w', encoding='utf-8') as f:
body = render_file('project.qhcp', outname=outname,
title=self.config.html_short_title,
homepage=homepage, startpage=startpage)
f.write(body)
def isdocnode(self, node):
# type: (nodes.Node) -> bool
if not isinstance(node, nodes.list_item):
return False
if len(node.children) != 2:
return False
if not isinstance(node[0], addnodes.compact_paragraph):
return False
if not isinstance(node[0][0], nodes.reference):
return False
if not isinstance(node[1], nodes.bullet_list):
return False
return True
def write_toc(self, node, indentlevel=4):
# type: (nodes.Node, int) -> List[str]
parts = [] # type: List[str]
if isinstance(node, nodes.list_item) and self.isdocnode(node):
compact_paragraph = cast(addnodes.compact_paragraph, node[0])
reference = cast(nodes.reference, compact_paragraph[0])
link = reference['refuri']
title = html.escape(reference.astext()).replace('"', '&quot;')
item = '<section title="%(title)s" ref="%(ref)s">' % \
{'title': title, 'ref': link}
parts.append(' ' * 4 * indentlevel + item)
bullet_list = cast(nodes.bullet_list, node[1])
list_items = cast(Iterable[nodes.list_item], bullet_list)
for list_item in list_items:
parts.extend(self.write_toc(list_item, indentlevel + 1))
parts.append(' ' * 4 * indentlevel + '</section>')
elif isinstance(node, nodes.list_item):
for subnode in node:
parts.extend(self.write_toc(subnode, indentlevel))
elif isinstance(node, nodes.reference):
link = node['refuri']
title = html.escape(node.astext()).replace('"', '&quot;')
item = section_template % {'title': title, 'ref': link}
item = ' ' * 4 * indentlevel + item
parts.append(item.encode('ascii', 'xmlcharrefreplace').decode())
elif isinstance(node, nodes.bullet_list):
for subnode in node:
parts.extend(self.write_toc(subnode, indentlevel))
elif isinstance(node, addnodes.compact_paragraph):
for subnode in node:
parts.extend(self.write_toc(subnode, indentlevel))
return parts
def keyword_item(self, name, ref):
# type: (str, Any) -> str
matchobj = _idpattern.match(name)
if matchobj:
groupdict = matchobj.groupdict()
shortname = groupdict['title']
id = groupdict.get('id')
# descr = groupdict.get('descr')
if shortname.endswith('()'):
shortname = shortname[:-2]
id = '%s.%s' % (id, shortname)
else:
id = None
nameattr = html.escape(name, quote=True)
refattr = html.escape(ref[1], quote=True)
if id:
item = ' ' * 12 + '<keyword name="%s" id="%s" ref="%s"/>' % (nameattr, id, refattr)
else:
item = ' ' * 12 + '<keyword name="%s" ref="%s"/>' % (nameattr, refattr)
item.encode('ascii', 'xmlcharrefreplace')
return item
def build_keywords(self, title, refs, subitems):
# type: (str, List[Any], Any) -> List[str]
keywords = [] # type: List[str]
# if len(refs) == 0: # XXX
# write_param('See Also', title)
if len(refs) == 1:
keywords.append(self.keyword_item(title, refs[0]))
elif len(refs) > 1:
for i, ref in enumerate(refs): # XXX
# item = (' '*12 +
# '<keyword name="%s [%d]" ref="%s"/>' % (
# title, i, ref))
# item.encode('ascii', 'xmlcharrefreplace')
# keywords.append(item)
keywords.append(self.keyword_item(title, ref))
if subitems:
for subitem in subitems:
keywords.extend(self.build_keywords(subitem[0], subitem[1], []))
return keywords
def get_project_files(self, outdir):
# type: (str) -> List[str]
if not outdir.endswith(os.sep):
outdir += os.sep
olen = len(outdir)
project_files = []
staticdir = path.join(outdir, '_static')
imagesdir = path.join(outdir, self.imagedir)
for root, dirs, files in os.walk(outdir):
resourcedir = root.startswith((staticdir, imagesdir))
for fn in sorted(files):
if (resourcedir and not fn.endswith('.js')) or fn.endswith('.html'):
filename = path.join(root, fn)[olen:]
project_files.append(canon_path(filename))
return project_files
deprecated_alias('sphinx.builders.qthelp',
{
'render_file': render_file,
'QtHelpBuilder': QtHelpBuilder,
},
RemovedInSphinx40Warning)
def setup(app):
# type: (Sphinx) -> Dict[str, Any]
app.setup_extension('sphinx.builders.html')
app.add_builder(QtHelpBuilder)
warnings.warn('sphinx.builders.qthelp has been moved to sphinxcontrib-qthelp.',
RemovedInSphinx40Warning)
app.add_config_value('qthelp_basename', lambda self: make_filename(self.project), None)
app.add_config_value('qthelp_namespace', None, 'html', [str])
app.add_config_value('qthelp_theme', 'nonav', 'html')
app.add_config_value('qthelp_theme_options', {}, 'html')
app.setup_extension('sphinxcontrib.qthelp')
return {
'version': 'builtin',
'version': sphinx.__display_version__,
'parallel_read_safe': True,
'parallel_write_safe': True,
}
-19
View File
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<QHelpCollectionProject version="1.0">
<assistant>
<title>{{ title|e }}</title>
<homePage>{{ homepage|e }}</homePage>
<startPage>{{ startpage|e }}</startPage>
</assistant>
<docFiles>
<generate>
<file>
<input>{{ outname|e }}.qhp</input>
<output>{{ outname|e }}.qch</output>
</file>
</generate>
<register>
<file>{{ outname|e }}.qch</file>
</register>
</docFiles>
</QHelpCollectionProject>
-26
View File
@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<QtHelpProject version="1.0">
<namespace>{{ namespace|e }}</namespace>
<virtualFolder>doc</virtualFolder>
<customFilter name="{{ project|e }} {{ version|e }}">
<filterAttribute>{{ outname|e }}</filterAttribute>
<filterAttribute>{{ version|e }}</filterAttribute>
</customFilter>
<filterSection>
<filterAttribute>{{ outname|e }}</filterAttribute>
<filterAttribute>{{ version|e }}</filterAttribute>
<toc>
<section title="{{ title|e }}" ref="{{ master_doc|e }}.html">
{{ sections }}
</section>
</toc>
<keywords>
{{ keywords }}
</keywords>
<files>
{%- for filename in files %}
<file>{{ filename|e }}</file>
{%- endfor %}
</files>
</filterSection>
</QtHelpProject>
+1 -1
View File
@@ -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', 'qthelp',
'dirhtml', 'singlehtml', 'pickle', 'json', 'text', 'htmlhelp',
'applehelp', 'changes', 'xml', 'pseudoxml', 'linkcheck',
],
)
-117
View File
@@ -1,117 +0,0 @@
"""
test_build_qthelp
~~~~~~~~~~~~~~~~~
Test the Qt Help builder and check its output. We don't need to
test the HTML itself; that's already handled by
:file:`test_build_html.py`.
:copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
import pytest
from sphinx.testing.util import etree_parse
@pytest.mark.sphinx('qthelp', testroot='basic')
def test_qthelp_basic(app, status, warning):
app.builder.build_all()
qhp = (app.outdir / 'Python.qhp').text()
assert '<customFilter name="Python ">' in qhp
assert '<filterAttribute>Python</filterAttribute>' in qhp
assert '<filterAttribute></filterAttribute>' in qhp
assert '<section title="Python documentation" ref="index.html">' in qhp
assert '<file>genindex.html</file>' in qhp
assert '<file>index.html</file>' in qhp
assert '<file>_static/basic.css</file>' in qhp
qhcp = (app.outdir / 'Python.qhcp').text()
assert '<title>Python documentation</title>' in qhcp
assert '<homePage>qthelp://org.sphinx.python/doc/index.html</homePage>' in qhcp
assert '<startPage>qthelp://org.sphinx.python/doc/index.html</startPage>' in qhcp
assert '<input>Python.qhp</input>' in qhcp
assert '<output>Python.qch</output>' in qhcp
assert '<file>Python.qch</file>' in qhcp
@pytest.mark.sphinx('qthelp', testroot='need-escaped')
def test_qthelp_escaped(app, status, warning):
app.builder.build_all()
et = etree_parse(app.outdir / 'needbescapedbproject.qhp')
customFilter = et.find('.//customFilter')
assert len(customFilter) == 2
assert customFilter.attrib == {'name': 'need <b>"escaped"</b> project '}
assert customFilter[0].text == 'needbescapedbproject'
assert customFilter[1].text is None
toc = et.find('.//toc')
assert len(toc) == 1
assert toc[0].attrib == {'title': 'need <b>"escaped"</b> project documentation',
'ref': 'index.html'}
assert len(toc[0]) == 4
assert toc[0][0].attrib == {'title': '<foo>', 'ref': 'foo.html'}
assert toc[0][0][0].attrib == {'title': 'quux', 'ref': 'quux.html'}
assert toc[0][0][1].attrib == {'title': 'foo "1"', 'ref': 'foo.html#foo-1'}
assert toc[0][0][1][0].attrib == {'title': 'foo.1-1', 'ref': 'foo.html#foo-1-1'}
assert toc[0][0][2].attrib == {'title': 'foo.2', 'ref': 'foo.html#foo-2'}
assert toc[0][1].attrib == {'title': 'bar', 'ref': 'bar.html'}
assert toc[0][2].attrib == {'title': 'http://sphinx-doc.org/',
'ref': 'http://sphinx-doc.org/'}
assert toc[0][3].attrib == {'title': 'baz', 'ref': 'baz.html'}
keywords = et.find('.//keywords')
assert len(keywords) == 2
assert keywords[0].attrib == {'name': '<subsection>', 'ref': 'index.html#index-0'}
assert keywords[1].attrib == {'name': '"subsection"', 'ref': 'index.html#index-0'}
@pytest.mark.sphinx('qthelp', testroot='basic')
def test_qthelp_namespace(app, status, warning):
# default namespace
app.builder.build_all()
qhp = (app.outdir / 'Python.qhp').text()
assert '<namespace>org.sphinx.python</namespace>' in qhp
qhcp = (app.outdir / 'Python.qhcp').text()
assert '<homePage>qthelp://org.sphinx.python/doc/index.html</homePage>' in qhcp
assert '<startPage>qthelp://org.sphinx.python/doc/index.html</startPage>' in qhcp
# give a namespace
app.config.qthelp_namespace = 'org.sphinx-doc.sphinx'
app.builder.build_all()
qhp = (app.outdir / 'Python.qhp').text()
assert '<namespace>org.sphinx-doc.sphinx</namespace>' in qhp
qhcp = (app.outdir / 'Python.qhcp').text()
assert '<homePage>qthelp://org.sphinx-doc.sphinx/doc/index.html</homePage>' in qhcp
assert '<startPage>qthelp://org.sphinx-doc.sphinx/doc/index.html</startPage>' in qhcp
@pytest.mark.sphinx('qthelp', testroot='basic')
def test_qthelp_title(app, status, warning):
# default title
app.builder.build_all()
qhp = (app.outdir / 'Python.qhp').text()
assert '<section title="Python documentation" ref="index.html">' in qhp
qhcp = (app.outdir / 'Python.qhcp').text()
assert '<title>Python documentation</title>' in qhcp
# give a title
app.config.html_title = 'Sphinx <b>"full"</b> title'
app.config.html_short_title = 'Sphinx <b>"short"</b> title'
app.builder.build_all()
qhp = (app.outdir / 'Python.qhp').text()
assert ('<section title="Sphinx &lt;b&gt;&#34;full&#34;&lt;/b&gt; title" ref="index.html">'
in qhp)
qhcp = (app.outdir / 'Python.qhcp').text()
assert '<title>Sphinx &lt;b&gt;&#34;short&#34;&lt;/b&gt; title</title>' in qhcp