Add manual page writer.

This commit is contained in:
Georg Brandl
2010-02-21 11:50:08 +01:00
parent d305ab2bd0
commit 121b864f31
16 changed files with 543 additions and 13 deletions

View File

@@ -8,6 +8,8 @@ Release 1.0 (in development)
* Support for docutils 0.4 has been removed.
* Added a manual page builder.
* New more compact doc field syntax is now recognized:
``:param type name: description``.

View File

@@ -21,6 +21,7 @@ help:
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " epub to make an epub file"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " man to make manual pages"
@echo " changes to make an overview over all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@@ -47,6 +48,11 @@ text:
@echo
@echo "Build finished."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) _build/man
@echo
@echo "Build finished."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) _build/pickle

View File

@@ -127,6 +127,22 @@ Note that a direct PDF builder using ReportLab is available in `rst2pdf
.. versionadded:: 0.4
.. module:: sphinx.builders.manpage
.. class:: ManualPageBuilder
This builder produces manual pages in the groff format. You have to specify
which documents are to be included in which manual pages via the
:confval:`man_pages` configuration value.
Its name is ``man``.
.. note::
This builder requires the docutils manual page writer, which is only
available as of docutils 0.6.
.. versionadded:: 1.0
.. currentmodule:: sphinx.builders.html
.. class:: SerializingHTMLBuilder

View File

@@ -49,6 +49,11 @@ latex_elements = {
todo_include_todos = True
man_pages = [
('contents', 'sphinx-all', 'Sphinx documentation generator system manual',
'Georg Brandl', 1),
]
# -- Extension interface -------------------------------------------------------

View File

@@ -914,6 +914,35 @@ These options influence LaTeX output.
Use the ``'pointsize'`` key in the :confval:`latex_elements` value.
.. _man-options:
Options for manual page output
------------------------------
These options influence manual page output.
.. confval:: man_pages
This value determines how to group the document tree into manual pages. It
must be a list of tuples ``(startdocname, name, description, authors,
section)``, where the items are:
* *startdocname*: document name that is the "root" of the manual page. All
documents referenced by it in TOC trees will be included in the manual file
too. (If you want one master manual page, use your :confval:`master_doc`
here.)
* *name*: name of the manual page. This should be a short string without
spaces or special characters. It is used to determine the file name as
well as the name of the manual page (in the NAME section).
* *description*: description of the manual page. This is used in the NAME
section.
* *authors*: A list of strings with authors, or a single string.
* *section*: The manual page section. Used for the output file name as well
as in the manual page header.
.. versionadded:: 1.0
.. rubric:: Footnotes
.. [1] A note on available globbing syntax: you can use the standard shell

View File

@@ -61,11 +61,11 @@ the following public API:
Register a Docutils node class. This is necessary for Docutils internals.
It may also be used in the future to validate nodes in the parsed documents.
Node visitor functions for the Sphinx HTML, LaTeX and text writers can be
given as keyword arguments: the keyword must be one or more of ``'html'``,
``'latex'``, ``'text'``, the value a 2-tuple of ``(visit, depart)`` methods.
``depart`` can be ``None`` if the ``visit`` function raises
:exc:`docutils.nodes.SkipNode`. Example:
Node visitor functions for the Sphinx HTML, LaTeX, text and manpage writers
can be given as keyword arguments: the keyword must be one or more of
``'html'``, ``'latex'``, ``'text'``, ``'man'``, the value a 2-tuple of
``(visit, depart)`` methods. ``depart`` can be ``None`` if the ``visit``
function raises :exc:`docutils.nodes.SkipNode`. Example:
.. code-block:: python

View File

@@ -351,6 +351,8 @@ class Sphinx(object):
from sphinx.writers.latex import LaTeXTranslator as translator
elif key == 'text':
from sphinx.writers.text import TextTranslator as translator
elif key == 'man':
from sphinx.writers.manpage import ManualPageTranslator as translator
else:
# ignore invalid keys for compatibility
continue

View File

@@ -326,6 +326,7 @@ BUILTIN_BUILDERS = {
'epub': ('epub', 'EpubBuilder'),
'latex': ('latex', 'LaTeXBuilder'),
'text': ('text', 'TextBuilder'),
'man': ('manpage', 'ManualPageBuilder'),
'changes': ('changes', 'ChangesBuilder'),
'linkcheck': ('linkcheck', 'CheckExternalLinksBuilder'),
}

View File

@@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
"""
sphinx.builders.manpage
~~~~~~~~~~~~~~~~~~~~~~~
Manual pages builder.
:copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
from os import path
from docutils.io import FileOutput
from docutils.frontend import OptionParser
from sphinx import addnodes
from sphinx.errors import SphinxError
from sphinx.builders import Builder
from sphinx.environment import NoUri
from sphinx.util.nodes import inline_all_toctrees
from sphinx.util.console import bold, darkgreen
try:
from sphinx.writers.manpage import ManualPageWriter
has_manpage_writer = True
except ImportError:
has_manpage_writer = False
class ManualPageBuilder(Builder):
"""
Builds groff output in manual page format.
"""
name = 'man'
format = 'man'
supported_image_types = []
def init(self):
if not has_manpage_writer:
raise SphinxError('The docutils manual page writer can\'t be '
'found; it is only available as of docutils 0.6.')
if not self.config.man_pages:
self.warn('no "man_pages" config value found; no manual pages '
'will be written')
def get_outdated_docs(self):
return 'all manpages' # for now
def get_target_uri(self, docname, typ=None):
if typ == 'token':
return ''
raise NoUri
def write(self, *ignored):
docwriter = ManualPageWriter(self)
docsettings = OptionParser(
defaults=self.env.settings,
components=(docwriter,)).get_default_values()
self.info(bold('writing... '), nonl=True)
for info in self.config.man_pages:
docname, name, description, authors, section = info
if isinstance(authors, basestring):
authors = [authors]
targetname = '%s.%s' % (name, section)
self.info(darkgreen(targetname) + ' { ', nonl=True)
destination = FileOutput(
destination_path=path.join(self.outdir, targetname),
encoding='utf-8')
tree = self.env.get_doctree(docname)
docnames = set()
largetree = inline_all_toctrees(self, docnames, docname, tree,
darkgreen)
self.info('} ', nonl=True)
self.env.resolve_references(largetree, docname, self)
# remove pending_xref nodes
for pendingnode in largetree.traverse(addnodes.pending_xref):
pendingnode.replace_self(pendingnode.children)
largetree.settings = docsettings
largetree.settings.title = name
largetree.settings.subtitle = description
largetree.settings.authors = authors
largetree.settings.section = section
docwriter.write(largetree, destination)
self.info()
def finish(self):
pass

View File

@@ -141,6 +141,9 @@ class Config(object):
# text options
text_sectionchars = ('*=-~"+`', 'text'),
text_windows_newlines = (False, 'text'),
# manpage options
man_pages = ([], None),
)
def __init__(self, dirname, filename, overrides, tags):

View File

@@ -486,11 +486,13 @@ def setup(app):
app.add_node(autosummary_toc,
html=(autosummary_toc_visit_html, autosummary_noop),
latex=(autosummary_noop, autosummary_noop),
text=(autosummary_noop, autosummary_noop))
text=(autosummary_noop, autosummary_noop),
man=(autosummary_noop, autosummary_noop))
app.add_node(autosummary_table,
html=(autosummary_table_visit_html, autosummary_noop),
latex=(autosummary_noop, autosummary_noop),
text=(autosummary_noop, autosummary_noop))
text=(autosummary_noop, autosummary_noop),
man=(autosummary_noop, autosummary_noop))
app.add_directive('autosummary', Autosummary)
app.add_role('autolink', autolink_role)
app.connect('doctree-read', process_autosummary_toc)

View File

@@ -159,7 +159,8 @@ def setup(app):
app.add_node(todo_node,
html=(visit_todo_node, depart_todo_node),
latex=(visit_todo_node, depart_todo_node),
text=(visit_todo_node, depart_todo_node))
text=(visit_todo_node, depart_todo_node),
man=(visit_todo_node, depart_todo_node))
app.add_directive('todo', Todo)
app.add_directive('todolist', TodoList)

View File

@@ -226,13 +226,25 @@ latex_documents = [
#latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('%(master_str)s', '%(project_manpage)s', u'%(project_doc)s',
[u'%(author_str)s'], 1)
]
'''
EPUB_CONFIG = '''
# -- Options for Epub output ---------------------------------------------------
# Bibliographic Dublin Core info.
#epub_title = ''
#epub_author = ''
#epub_publisher = ''
#epub_copyright = ''
epub_title = u'%(project_str)s'
epub_author = u'%(author_str)s'
epub_publisher = u'%(author_str)s'
epub_copyright = u'%(copyright_str)s'
# The language of the text. It defaults to the language option
# or en if the language is not set.
@@ -324,6 +336,8 @@ help:
\t@echo " epub to make an epub"
\t@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
\t@echo " latexpdf to make LaTeX files and run them through pdflatex"
\t@echo " text to make text files"
\t@echo " man to make manual pages"
\t@echo " changes to make an overview of all changed/added/deprecated items"
\t@echo " linkcheck to check all external links for integrity"
\t@echo " doctest to run all doctests embedded in the documentation \
@@ -400,6 +414,16 @@ latexpdf: latex
\tmake -C %(rbuilddir)s/latex all-pdf
\t@echo "pdflatex finished; the PDF files are in %(rbuilddir)s/latex."
text:
\t$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
\t@echo
\t@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
\t$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
\t@echo
\t@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
changes:
\t$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
\t@echo
@@ -444,6 +468,8 @@ if "%%1" == "help" (
\techo. devhelp to make HTML files and a Devhelp project
\techo. epub to make an epub
\techo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
\techo. text to make text files
\techo. man to make manual pages
\techo. changes to make an overview over all changed/added/deprecated items
\techo. linkcheck to check all external links for integrity
\techo. doctest to run all doctests embedded in the documentation if enabled
@@ -531,6 +557,20 @@ if "%%1" == "latex" (
\tgoto end
)
if "%%1" == "text" (
\t%%SPHINXBUILD%% -b text %%ALLSPHINXOPTS%% %%BUILDDIR%%/text
\techo.
\techo.Build finished. The text files are in %%BUILDDIR%%/text.
\tgoto end
)
if "%%1" == "man" (
\t%%SPHINXBUILD%% -b man %%ALLSPHINXOPTS%% %%BUILDDIR%%/man
\techo.
\techo.Build finished. The manual pages are in %%BUILDDIR%%/man.
\tgoto end
)
if "%%1" == "changes" (
\t%%SPHINXBUILD%% -b changes %%ALLSPHINXOPTS%% %%BUILDDIR%%/changes
\techo.
@@ -703,6 +743,11 @@ document is a custom template, you can also set this to another filename.'''
do_prompt(d, 'master', 'Please enter a new file name, or rename the '
'existing file and press Enter', d['master'])
print '''
Sphinx can also add configuration for epub output:'''
do_prompt(d, 'epub', 'Do you want to use the epub builder (y/N)',
'n', boolean)
print '''
Please indicate if you want to use one of the following Sphinx extensions:'''
do_prompt(d, 'ext_autodoc', 'autodoc: automatically insert docstrings '
@@ -735,6 +780,7 @@ directly.'''
'y', boolean)
d['project_fn'] = make_filename(d['project'])
d['project_manpage'] = d['project_fn'].lower()
d['now'] = time.asctime()
d['underline'] = len(d['project']) * '='
d['extensions'] = ', '.join(
@@ -751,7 +797,7 @@ directly.'''
# escape backslashes and single quotes in strings that are put into
# a Python string literal
for key in ('project', 'copyright', 'author_texescaped',
for key in ('project', 'copyright', 'author', 'author_texescaped',
'project_doc_texescaped', 'version', 'release', 'master'):
d[key + '_str'] = d[key].replace('\\', '\\\\').replace("'", "\\'")
@@ -772,6 +818,8 @@ directly.'''
mkdir_p(path.join(srcdir, d['dot'] + 'static'))
conf_text = QUICKSTART_CONF % d
if d['epub']:
conf_text += EPUB_CONFIG % d
if d['ext_intersphinx']:
conf_text += INTERSPHINX_CONFIG

308
sphinx/writers/manpage.py Normal file
View File

@@ -0,0 +1,308 @@
# -*- coding: utf-8 -*-
"""
sphinx.writers.manpage
~~~~~~~~~~~~~~~~~~~~~~
Manual page writer, extended for Sphinx custom nodes.
:copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
from docutils import nodes
from docutils.writers.manpage import MACRO_DEF, Writer, \
Translator as BaseTranslator
from sphinx import addnodes
from sphinx.locale import admonitionlabels, versionlabels, _
from sphinx.util.osutil import ustrftime
class ManualPageWriter(Writer):
def __init__(self, builder):
Writer.__init__(self)
self.builder = builder
def translate(self):
visitor = ManualPageTranslator(self.builder, self.document)
self.visitor = visitor
self.document.walkabout(visitor)
self.output = visitor.astext()
class ManualPageTranslator(BaseTranslator):
"""
Custom translator.
"""
def __init__(self, builder, *args, **kwds):
BaseTranslator.__init__(self, *args, **kwds)
self.builder = builder
self.in_productionlist = 0
# first title is the manpage title
self.section_level = -1
# docinfo set by man_pages config value
self._docinfo['title'] = self.document.settings.title
self._docinfo['subtitle'] = self.document.settings.subtitle
self._docinfo['author'] = self.document.settings.authors
self._docinfo['manual_section'] = self.document.settings.section
# docinfo set by other config values
self._docinfo['title_upper'] = self._docinfo['title'].upper()
if builder.config.today:
self._docinfo['date'] = builder.config.today
else:
self._docinfo['date'] = ustrftime(builder.config.today_fmt
or _('%B %d, %Y'))
self._docinfo['copyright'] = builder.config.copyright
self._docinfo['version'] = builder.config.version
self._docinfo['manual_group'] = builder.config.project
# since self.append_header() is never called, need to do this here
self.body.append(MACRO_DEF)
# overwritten -- added quotes around all .TH arguments
def header(self):
tmpl = (".TH \"%(title_upper)s\" \"%(manual_section)s\""
" \"%(date)s\" \"%(version)s\" \"%(manual_group)s\"\n"
".SH NAME\n"
"%(title)s \- %(subtitle)s\n")
return tmpl % self._docinfo
def visit_start_of_file(self, node):
pass
def depart_start_of_file(self, node):
pass
def visit_desc(self, node):
self.visit_definition_list(node)
def depart_desc(self, node):
self.depart_definition_list(node)
def visit_desc_signature(self, node):
self.visit_definition_list_item(node)
self.visit_term(node)
def depart_desc_signature(self, node):
self.depart_term(node)
def visit_desc_addname(self, node):
pass
def depart_desc_addname(self, node):
pass
def visit_desc_type(self, node):
pass
def depart_desc_type(self, node):
pass
def visit_desc_returns(self, node):
self.body.append(' -> ')
def depart_desc_returns(self, node):
pass
def visit_desc_name(self, node):
pass
def depart_desc_name(self, node):
pass
def visit_desc_parameterlist(self, node):
self.body.append('(')
self.first_param = 1
def depart_desc_parameterlist(self, node):
self.body.append(')')
def visit_desc_parameter(self, node):
if not self.first_param:
self.body.append(', ')
else:
self.first_param = 0
def depart_desc_parameter(self, node):
pass
def visit_desc_optional(self, node):
self.body.append('[')
def depart_desc_optional(self, node):
self.body.append(']')
def visit_desc_annotation(self, node):
pass
def depart_desc_annotation(self, node):
pass
def visit_desc_content(self, node):
self.visit_definition(node)
def depart_desc_content(self, node):
self.depart_definition(node)
def visit_refcount(self, node):
self.body.append(self.defs['emphasis'][0])
def depart_refcount(self, node):
self.body.append(self.defs['emphasis'][1])
def visit_versionmodified(self, node):
self.visit_paragraph(node)
text = versionlabels[node['type']] % node['version']
if len(node):
text += ': '
else:
text += '.'
self.body.append(text)
def depart_versionmodified(self, node):
self.depart_paragraph(node)
# overwritten -- we don't want source comments to show up
def visit_comment(self, node):
raise nodes.SkipNode
# overwritten -- added ensure_eol()
def visit_footnote(self, node):
self.ensure_eol()
BaseTranslator.visit_footnote(self, node)
# overwritten -- handle footnotes rubric
def visit_rubric(self, node):
self.ensure_eol()
if len(node.children) == 1:
rubtitle = node.children[0].astext()
if rubtitle in ('Footnotes', _('Footnotes')):
self.body.append('.SH ' + self.deunicode(rubtitle).upper() +
'\n')
raise nodes.SkipNode
else:
self.body.append('.sp\n')
def depart_rubric(self, node):
pass
def visit_seealso(self, node):
self.visit_admonition(node)
def depart_seealso(self, node):
self.depart_admonition(node)
# overwritten -- use our own label translations
def visit_admonition(self, node, name=None):
if name:
self.body.append('.IP %s\n' %
admonitionlabels.get(name, name))
def visit_productionlist(self, node):
self.ensure_eol()
names = []
self.in_productionlist += 1
self.body.append('.sp\n.nf\n')
for production in node:
names.append(production['tokenname'])
maxlen = max(len(name) for name in names)
for production in node:
if production['tokenname']:
lastname = production['tokenname'].ljust(maxlen)
self.body.append(self.defs['strong'][0])
self.body.append(self.deunicode(lastname))
self.body.append(self.defs['strong'][1])
self.body.append(' ::= ')
else:
self.body.append('%s ' % (' '*len(lastname)))
production.walkabout(self)
self.body.append('\n')
self.body.append('\n.fi\n')
self.in_productionlist -= 1
raise nodes.SkipNode
def visit_production(self, node):
pass
def depart_production(self, node):
pass
# overwritten -- don't visit inner marked up nodes
def visit_reference(self, node):
self.body.append(self.defs['reference'][0])
self.body.append(node.astext())
self.body.append(self.defs['reference'][1])
raise nodes.SkipNode
def visit_centered(self, node):
self.ensure_eol()
self.body.append('.sp\n.ce\n')
def depart_centered(self, node):
self.body.append('\n.ce 0\n')
def visit_compact_paragraph(self, node):
pass
def depart_compact_paragraph(self, node):
pass
def visit_highlightlang(self, node):
pass
def depart_highlightlang(self, node):
pass
def visit_download_reference(self, node):
pass
def depart_download_reference(self, node):
pass
def visit_toctree(self, node):
raise nodes.SkipNode
def visit_index(self, node):
raise nodes.SkipNode
def visit_tabular_col_spec(self, node):
raise nodes.SkipNode
def visit_glossary(self, node):
pass
def depart_glossary(self, node):
pass
def visit_acks(self, node):
self.ensure_eol()
self.body.append(', '.join(n.astext()
for n in node.children[0].children) + '.')
self.body.append('\n')
raise nodes.SkipNode
def visit_hlist(self, node):
self.visit_bullet_list(node)
def depart_hlist(self, node):
self.depart_bullet_list(node)
def visit_hlistcol(self, node):
pass
def depart_hlistcol(self, node):
pass
def visit_literal_emphasis(self, node):
return self.visit_emphasis(node)
def depart_literal_emphasis(self, node):
return self.depart_emphasis(node)
def visit_abbreviation(self, node):
pass
def depart_abbreviation(self, node):
pass
# overwritten: handle section titles better than in 0.6 release
def visit_title(self, node):
if isinstance(node.parent, addnodes.seealso):
self.body.append('.IP "')
return
elif isinstance(node.parent, nodes.section):
if self.section_level == 0:
# skip the document title
raise nodes.SkipNode
elif self.section_level == 1:
self.body.append('.SH %s\n' %
self.deunicode(node.astext().upper()))
raise nodes.SkipNode
return BaseTranslator.visit_title(self, node)
def depart_title(self, node):
if isinstance(node.parent, addnodes.seealso):
self.body.append('"\n')
return
return BaseTranslator.depart_title(self, node)
def unknown_visit(self, node):
raise NotImplementedError('Unknown node: ' + node.__class__.__name__)

View File

@@ -45,6 +45,15 @@ def test_epub(app):
def test_changes(app):
app.builder.build_all()
try:
from docutils.writers.manpage import Writer
except ImportError:
pass
else:
@with_app(buildername='man')
def test_man(app):
app.builder.build_all()
@with_app(buildername='singlehtml', cleanenv=True)
def test_singlehtml(app):
app.builder.build_all()

View File

@@ -129,6 +129,7 @@ def test_quickstart_all_answers(tempdir):
'viewcode': 'no',
'Create Makefile': 'no',
'Create Windows command file': 'no',
'Do you want to use the epub builder': 'yes',
}
qs.raw_input = mock_raw_input(answers, needanswer=True)
qs.TERM_ENCODING = 'utf-8'
@@ -151,6 +152,10 @@ def test_quickstart_all_answers(tempdir):
assert ns['latex_documents'] == [
('contents', 'STASI.tex', u'STASI™ Documentation',
u'Wolfgang Schäuble \\& G\'Beckstein', 'manual')]
assert ns['epub_author'] == u'Wolfgang Schäuble & G\'Beckstein'
assert ns['man_pages'] == [
('contents', 'stasi', u'STASI™ Documentation',
[u'Wolfgang Schäuble & G\'Beckstein'], 1)]
assert (tempdir / 'build').isdir()
assert (tempdir / 'source' / '.static').isdir()