diff --git a/doc/Makefile b/doc/Makefile index aa3ecb61a..0199ba47c 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -29,6 +29,8 @@ help: @echo " epub to make an epub file" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run pdflatex" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview over all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @@ -131,3 +133,16 @@ linkcheck: doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) _build/doctest + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) _build/texinfo + @echo + @echo "Build finished. The Texinfo files are in _build/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) _build/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C _build/texinfo info + @echo "makeinfo finished; the Info files are in _build/texinfo." diff --git a/doc/builders.rst b/doc/builders.rst index 8ea46ad62..71e28a7ca 100644 --- a/doc/builders.rst +++ b/doc/builders.rst @@ -144,6 +144,27 @@ Note that a direct PDF builder using ReportLab is available in `rst2pdf .. versionadded:: 1.0 + +.. module:: sphinx.builders.texinfo +.. class:: TexinfoBuilder + + This builder produces Texinfo files that can be processed into Info + files by the :program:`makeinfo` program. You have to specify which + documents are to be included in which Texinfo files via the + :confval:`texinfo_documents` configuration value. + + The Info format is the basis of the on-line help system used by GNU + Emacs and the terminal-based program :program:`info`. See + :ref:`texinfo-faq` for more details. The Texinfo format is the + official documentation system used by the GNU project. More + information on Texinfo can be found at + ``_. + + Its name is ``texinfo``. + + .. versionadded:: 1.1 + + .. currentmodule:: sphinx.builders.html .. class:: SerializingHTMLBuilder diff --git a/doc/conf.py b/doc/conf.py index b3a1cda79..dd23b0812 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -64,6 +64,12 @@ man_pages = [ 'template generator', '', 1), ] +texinfo_documents = [ + ('contents', 'sphinx', 'Sphinx Documentation', 'Georg Brandl', + 'Sphinx', 'The Sphinx documentation builder.', 'Documentation tools', + 1), +] + # We're not using intersphinx right now, but if we did, this would be part of # the mapping: intersphinx_mapping = {'python': ('http://docs.python.org/dev', None)} diff --git a/doc/config.rst b/doc/config.rst index 906a9e199..493a7d83e 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -1040,6 +1040,79 @@ These options influence manual page output. .. versionadded:: 1.0 +.. _texinfo-options: + +Options for Texinfo output +-------------------------- + +These options influence Texinfo output. + +.. confval:: texinfo_documents + + This value determines how to group the document tree into Texinfo + source files. It must be a list of tuples ``(startdocname, + targetname, title, author, dir_entry, description, category, + toctree_only)``, where the items are: + + * *startdocname*: document name that is the "root" of the Texinfo + file. All documents referenced by it in TOC trees will be + included in the Texinfo file too. (If you want only one Texinfo + file, use your :confval:`master_doc` here.) + * *targetname*: file name (no extension) of the Texinfo file in + the output directory. + * *title*: Texinfo document title. Can be empty to use the title of the + *startdoc*. + * *author*: Author for the Texinfo document. Use ``\and`` to + separate multiple authors, as in: ``'John \and Sarah'``. + * *dir_entry*: The name that will appear in the top-level ``DIR`` + menu file. + * *description*: Descriptive text to appear in the top-level + ``DIR`` menu file. + * *category*: Specifies the section which this entry will appear in + the top-level ``DIR`` menu file. + * *toctree_only*: Must be ``True`` or ``False``. If ``True``, the + *startdoc* document itself is not included in the output, only + the documents referenced by it via TOC trees. With this option, + you can put extra stuff in the master document that shows up in + the HTML, but not the Texinfo output. + + .. versionadded:: 1.1 + + +.. confval:: texinfo_appendices + + A list of document names to append as an appendix to all manuals. + + .. versionadded:: 1.1 + + +.. confval:: texinfo_elements + + A dictionary that contains Texinfo snippets that override those Sphinx usually + puts into the generated ``.texi`` files. + + * Keys that you may want to override include: + + ``'paragraphindent'`` + Number of spaces to indent the first line of each paragraph, + default ``2``. Specify ``0`` for no indentation. + + ``'exampleindent'`` + Number of spaces to indent the lines for examples or literal blocks, default ``4``. + Specify ``0`` for no indentation. + + ``'preamble'`` + Text inserted as is near the beginning of the file. + + * Keys that are set by other options and therefore should not be overridden are: + + ``'filename'`` + ``'title'`` + ``'direntry'`` + + .. versionadded:: 1.1 + + .. rubric:: Footnotes .. [1] A note on available globbing syntax: you can use the standard shell diff --git a/doc/faq.rst b/doc/faq.rst index 5869e3af8..bd516a5fa 100644 --- a/doc/faq.rst +++ b/doc/faq.rst @@ -148,3 +148,112 @@ some notes: .. _Calibre: http://calibre-ebook.com/ .. _FBreader: http://www.fbreader.org/ .. _Bookworm: http://bookworm.oreilly.com/ + + +.. _texinfo-faq: + +Texinfo info +------------ + +The Texinfo builder is currently in an experimental stage but has +successfully been used to build the documentation for both Sphinx and +Python. The intended use of this builder is to generate Texinfo that +is then processed into Info files. + +There are two main programs for reading Info files, ``info`` and GNU +Emacs. The ``info`` program has less features but is available in +most Unix environments and can be quickly accessed from the terminal. +Emacs provides better font and color display and supports extensive +customization (of course). + +.. _texinfo-links: + +Displaying Links +~~~~~~~~~~~~~~~~ + +One noticeable problem you may encounter with the generated Info files +is how references are displayed. If you read the source of an Info +file, a reference to this section would look like:: + + * note Displaying Links: target-id + +In the stand-alone reader, ``info``, references are displayed just as +they appear in the source. Emacs, on the other-hand, will by default +replace ``\*note:`` with ``see`` and hide the ``target-id``. For +example: + + :ref:`texinfo-links` + +The exact behavior of how Emacs displays references is dependent on +the variable ``Info-hide-note-references``. If set to the value of +``hide``, Emacs will hide both the ``\*note:`` part and the +``target-id``. This is generally the best way to view Sphinx-based +documents since they often make frequent use of links and do not take +this limitation into account. However, changing this variable affects +how all Info documents are displayed and most due take this behavior +into account. + +If you want Emacs to display Info files produced by Sphinx using the +value ``hide`` for ``Info-hide-note-references`` and the default value +for all other Info files, try adding the following Emacs Lisp code to +your start-up file, ``~/.emacs.d/init.el``. + +:: + + (defadvice info-insert-file-contents (after + sphinx-info-insert-file-contents + activate) + "Hack to make `Info-hide-note-references' buffer-local and + automatically set to `hide' iff it can be determined that this file + was created from a Texinfo file generated by Docutils or Sphinx." + (set (make-local-variable 'Info-hide-note-references) + (default-value 'Info-hide-note-references)) + (save-excursion + (save-restriction + (widen) (goto-char (point-min)) + (when (re-search-forward + "^Generated by \\(Sphinx\\|Docutils\\)" + (save-excursion (search-forward "" nil t)) t) + (set (make-local-variable 'Info-hide-note-references) + 'hide))))) + + +Notes +~~~~~ + +The following notes may be helpful if you want to create Texinfo +files: + +- Each section corresponds to a different ``node`` in the Info file. + +- Some characters cannot be properly escaped in menu entries and + xrefs. The following characters are replaced by spaces in these + contexts: ``@``, ``{``, ``}``, ``.``, ``,``, and ``:``. + +- In the HTML and Tex output, the word ``see`` is automatically + inserted before all xrefs. + +- Links to external Info files can be created using the somewhat + official URI scheme ``info``. For example:: + + info:Texinfo#makeinfo_options + + which produces: + + info:Texinfo#makeinfo_options + +- Inline markup appears as follows in Info: + + * strong -- \*strong\* + * emphasis -- _emphasis_ + * literal -- \`literal' + + It is possible to change this behavior using the Texinfo command + ``@definfoenclose``. For example, to make inline markup more + closely resemble reST, add the following to your :file:`conf.py`:: + + texinfo_elements = {'preamble': """\ + @definfoenclose strong,**,** + @definfoenclose emph,*,* + @definfoenclose code,`@w{}`,`@w{}` + """} diff --git a/doc/invocation.rst b/doc/invocation.rst index e143a3233..c8e9a61fc 100644 --- a/doc/invocation.rst +++ b/doc/invocation.rst @@ -40,6 +40,10 @@ The :program:`sphinx-build` script has several options: **man** Build manual pages in groff format for UNIX systems. + **texinfo** + Build Texinfo files that can be processed into Info files using + :program:`makeinfo`. + **text** Build plain text files. diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index ce04f7691..1fe474075 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -325,6 +325,7 @@ BUILTIN_BUILDERS = { 'latex': ('latex', 'LaTeXBuilder'), 'text': ('text', 'TextBuilder'), 'man': ('manpage', 'ManualPageBuilder'), + 'texinfo': ('texinfo', 'TexinfoBuilder'), 'changes': ('changes', 'ChangesBuilder'), 'linkcheck': ('linkcheck', 'CheckExternalLinksBuilder'), 'websupport': ('websupport', 'WebSupportBuilder'), diff --git a/sphinx/builders/texinfo.py b/sphinx/builders/texinfo.py new file mode 100644 index 000000000..d65a46810 --- /dev/null +++ b/sphinx/builders/texinfo.py @@ -0,0 +1,229 @@ +# -*- coding: utf-8 -*- +""" + sphinx.builders.texinfo + ~~~~~~~~~~~~~~~~~~~~~~~ + + Texinfo builder. + + :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import os +from os import path + +from docutils import nodes +from docutils.io import FileOutput +from docutils.utils import new_document +from docutils.frontend import OptionParser + +from sphinx import package_dir, addnodes +from sphinx.locale import _ +from sphinx.builders import Builder +from sphinx.environment import NoUri +from sphinx.util.nodes import inline_all_toctrees +from sphinx.util.osutil import SEP, copyfile +from sphinx.util.console import bold, darkgreen +from sphinx.writers.texinfo import TexinfoWriter + + +TEXINFO_MAKEFILE = '''\ +# Makefile for Sphinx Texinfo output + +infodir ?= /usr/share/info + +MAKEINFO = makeinfo --no-split +MAKEINFO_html = makeinfo --no-split --html +MAKEINFO_plaintext = makeinfo --no-split --plaintext +TEXI2PDF = texi2pdf --batch --expand +INSTALL_INFO = install-info + +ALLDOCS = $(basename $(wildcard *.texi)) + +all: info +info: $(addsuffix .info,$(ALLDOCS)) +plaintext: $(addsuffix .txt,$(ALLDOCS)) +html: $(addsuffix .html,$(ALLDOCS)) +pdf: $(addsuffix .pdf,$(ALLDOCS)) + +install-info: info +\tfor f in *.info; do \\ +\t cp -t $(infodir) "$$f" && \\ +\t $(INSTALL_INFO) --info-dir=$(infodir) "$$f" ;\\ +\tdone + +uninstall-info: info +\tfor f in *.info; do \\ +\t rm -f "$(infodir)/$$f" ;\\ +\t $(INSTALL_INFO) --delete --info-dir=$(infodir) "$$f" ;\\ +\tdone + +%.info: %.texi +\t$(MAKEINFO) -o '$@' '$<' + +%.txt: %.texi +\t$(MAKEINFO_plaintext) -o '$@' '$<' + +%.html: %.texi +\t$(MAKEINFO_html) -o '$@' '$<' + +%.pdf: %.texi +\t-$(TEXI2PDF) '$<' +\t-$(TEXI2PDF) '$<' +\t-$(TEXI2PDF) '$<' + +clean: +\t-rm -f *.info *.pdf *.txt *.html +\t-rm -f *.log *.ind *.aux *.toc *.syn *.idx *.out *.ilg *.pla *.ky *.pg +\t-rm -f *.vr *.tp *.fn *.fns *.def *.defs *.cp *.cps *.ge *.ges *.mo + +.PHONY: all info plaintext html pdf install-info uninstall-info clean +''' + + +class TexinfoBuilder(Builder): + """ + Builds Texinfo output to create Info. + """ + name = 'texinfo' + format = 'texinfo' + supported_image_types = [] + + def init(self): + self.docnames = [] + self.document_data = [] + + def get_outdated_docs(self): + return 'all documents' # for now + + def get_target_uri(self, docname, typ=None): + if docname not in self.docnames: + raise NoUri + else: + return '%' + docname + + def get_relative_uri(self, from_, to, typ=None): + # ignore source path + return self.get_target_uri(to, typ) + + def init_document_data(self): + preliminary_document_data = map(list, self.config.texinfo_documents) + if not preliminary_document_data: + self.warn('no "texinfo_documents" config value found; no documents ' + 'will be written') + return + # assign subdirs to titles + self.titles = [] + for entry in preliminary_document_data: + docname = entry[0] + if docname not in self.env.all_docs: + self.warn('"texinfo_documents" config value references unknown ' + 'document %s' % docname) + continue + self.document_data.append(entry) + if docname.endswith(SEP+'index'): + docname = docname[:-5] + self.titles.append((docname, entry[2])) + + def write(self, *ignored): + self.init_document_data() + for entry in self.document_data: + docname, targetname, title, author = entry[:4] + targetname += '.texi' + direntry = description = category = '' + if len(entry) > 6: + direntry, description, category = entry[4:7] + toctree_only = False + if len(entry) > 7: + toctree_only = entry[7] + destination = FileOutput( + destination_path=path.join(self.outdir, targetname), + encoding='utf-8') + self.info("processing " + targetname + "... ", nonl=1) + doctree = self.assemble_doctree(docname, toctree_only, + appendices=(self.config.texinfo_appendices or [])) + self.info("writing... ", nonl=1) + + # Add an Index section + if self.config.texinfo_domain_indices: + doctree.append( + nodes.section('', + nodes.title(_("Index"), + nodes.Text(_('Index'), + _('Index'))), + nodes.raw('@printindex ge\n', + nodes.Text('@printindex ge\n', + '@printindex ge\n'), + format="texinfo"))) + docwriter = TexinfoWriter(self) + settings = OptionParser( + defaults=self.env.settings, + components=(docwriter,)).get_default_values() + settings.author = author + settings.title = title + settings.texinfo_filename = targetname[:-5] + '.info' + settings.texinfo_elements = self.config.texinfo_elements + settings.texinfo_dir_entry = direntry or '' + settings.texinfo_dir_category = category or '' + settings.texinfo_dir_description = description or '' + settings.docname = docname + doctree.settings = settings + docwriter.write(doctree, destination) + self.info("done") + + def assemble_doctree(self, indexfile, toctree_only, appendices): + self.docnames = set([indexfile] + appendices) + self.info(darkgreen(indexfile) + " ", nonl=1) + tree = self.env.get_doctree(indexfile) + tree['docname'] = indexfile + if toctree_only: + # extract toctree nodes from the tree and put them in a + # fresh document + new_tree = new_document('') + new_sect = nodes.section() + new_sect += nodes.title(u'', + u'') + new_tree += new_sect + for node in tree.traverse(addnodes.toctree): + new_sect += node + tree = new_tree + largetree = inline_all_toctrees(self, self.docnames, indexfile, tree, + darkgreen) + largetree['docname'] = indexfile + for docname in appendices: + appendix = self.env.get_doctree(docname) + appendix['docname'] = docname + largetree.append(appendix) + self.info() + self.info("resolving references...") + self.env.resolve_references(largetree, indexfile, self) + # TODO: add support for external :ref:s + for pendingnode in largetree.traverse(addnodes.pending_xref): + docname = pendingnode['refdocname'] + sectname = pendingnode['refsectname'] + newnodes = [nodes.emphasis(sectname, sectname)] + for subdir, title in self.titles: + if docname.startswith(subdir): + newnodes.append(nodes.Text(_(' (in '), _(' (in '))) + newnodes.append(nodes.emphasis(title, title)) + newnodes.append(nodes.Text(')', ')')) + break + else: + pass + pendingnode.replace_self(newnodes) + return largetree + + def finish(self): + self.info(bold('copying Texinfo support files... '), nonl=True) + # copy Makefile + fn = path.join(self.outdir, 'Makefile') + self.info(fn, nonl=1) + try: + mkfile = open(fn, 'w') + try: + mkfile.write(TEXINFO_MAKEFILE) + finally: + mkfile.close() + except (IOError, OSError), err: + self.warn("error writing file %s: %s" % (fn, err)) + self.info(' done') diff --git a/sphinx/config.py b/sphinx/config.py index 8ad260e9c..a0ba989a9 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -159,6 +159,12 @@ class Config(object): # manpage options man_pages = ([], None), + + # Texinfo options + texinfo_documents = ([], None), + texinfo_appendices = ([], None), + texinfo_elements = ({}, None), + texinfo_domain_indices = (True, None), ) def __init__(self, dirname, filename, overrides, tags): diff --git a/sphinx/quickstart.py b/sphinx/quickstart.py index 864cca95d..bba3c66df 100644 --- a/sphinx/quickstart.py +++ b/sphinx/quickstart.py @@ -254,6 +254,19 @@ man_pages = [ ('%(master_str)s', '%(project_manpage)s', u'%(project_doc)s', [u'%(author_str)s'], 1) ] + +# -- Options for Texinfo output ------------------------------------------------ + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('%(master_str)s', '%(project_fn)s', u'%(project_doc)s', u'%(author_str)s', + '%(project_fn)s', 'One line description of project.', 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +texinfo_appendices = [] ''' EPUB_CONFIG = ''' @@ -364,6 +377,8 @@ help: \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 " texinfo to make Texinfo files" +\t@echo " info to make Texinfo files and run them through makeinfo" \t@echo " gettext to make PO message catalogs" \t@echo " changes to make an overview of all changed/added/deprecated items" \t@echo " linkcheck to check all external links for integrity" @@ -451,6 +466,19 @@ man: \t@echo \t@echo "Build finished. The manual pages are in $(BUILDDIR)/man." +texinfo: +\t$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo +\t@echo +\t@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." +\t@echo "Run \\`make' in that directory to run these through makeinfo" \\ +\t "(use \\`make info' here to do that automatically)." + +info: +\t$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo +\t@echo "Running Texinfo files through makeinfo..." +\tmake -C $(BUILDDIR)/texinfo info +\t@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + gettext: \t$(SPHINXBUILD) -b gettext $(ALLSPHINXOPTS) $(BUILDDIR)/locale \t@echo diff --git a/sphinx/writers/texinfo.py b/sphinx/writers/texinfo.py new file mode 100644 index 000000000..c3b8fdc0c --- /dev/null +++ b/sphinx/writers/texinfo.py @@ -0,0 +1,1186 @@ +# -*- coding: utf-8 -*- +""" + sphinx.writers.texinfo + ~~~~~~~~~~~~~~~~~~~~~~ + + Custom docutils writer for Texinfo. + + :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import docutils +from docutils import nodes, writers + +from sphinx import addnodes +from sphinx.locale import versionlabels + + +TEMPLATE = """\ +\\input texinfo @c -*-texinfo-*- +@c %%**start of header +@setfilename %(filename)s +@documentencoding UTF-8 +@copying +Generated by Sphinx +@end copying +@settitle %(title)s +@defindex ge +@paragraphindent %(paragraphindent)s +@exampleindent %(exampleindent)s +%(direntry)s +@c %%**end of header + +@titlepage +@title %(title)s +@author %(author)s +@end titlepage +@contents + +@c %%** start of user preamble +%(preamble)s +@c %%** end of user preamble + +@ifnottex +@node Top +@top %(title)s +@end ifnottex + +@c %%**start of body +%(body)s +@c %%**end of body +@bye +""" + + +def find_subsections(section): + """Return a list of subsections for the given ``section``.""" + result = [] + for child in section.children: + if isinstance(child, nodes.section): + result.append(child) + continue + result.extend(find_subsections(child)) + return result + + +## Escaping +# Which characters to escape depends on the context. In some cases, +# namely menus and node names, it's not possible to escape certain +# characters. + +def escape(s): + """Return a string with Texinfo command characters escaped.""" + s = s.replace('@', '@@') + s = s.replace('{', '@{') + s = s.replace('}', '@}') + # Prevent "--" from being converted to an "em dash" + # s = s.replace('-', '@w{-}') + return s + +def escape_arg(s): + """Return an escaped string suitable for use as an argument + to a Texinfo command.""" + s = escape(s) + # Commas are the argument delimeters + s = s.replace(',', '@comma{}') + # Normalize white space + s = ' '.join(s.split()).strip() + return s + +def escape_id(s): + """Return an escaped string suitable for node names, menu entries, + and xrefs anchors.""" + bad_chars = ',:.()@{}' + for bc in bad_chars: + s = s.replace(bc, ' ') + s = ' '.join(s.split()).strip() + return s + + +class TexinfoWriter(writers.Writer): + """Texinfo writer for generating Texinfo documents.""" + supported = ('texinfo', 'texi') + + settings_spec = ( + 'Texinfo Specific Options', + None, + ( + ("Name of the resulting Info file to be created by 'makeinfo'. " + "Should probably end with '.info'.", + ['--texinfo-filename'], + {'default': '', 'metavar': ''}), + + ('Specify the Info dir entry category.', + ['--texinfo-dir-category'], + {'default': 'Miscellaneous', 'metavar': ''}), + + ('The name to use for the Info dir entry. ' + 'If not provided, no entry will be created.', + ['--texinfo-dir-entry'], + {'default': '', 'metavar': ''}), + + ('A brief description (one or two lines) to use for the ' + 'Info dir entry.', + ['--texinfo-dir-description'], + {'default': '', 'metavar': ''}), + ) + ) + + settings_defaults = {} + settings_default_overrides = {'docinfo_xform': 0} + + output = None + + visitor_attributes = ('output', 'fragment') + + def __init__(self, builder): + writers.Writer.__init__(self) + self.builder = builder + + def translate(self): + self.visitor = visitor = TexinfoTranslator(self.document, self.builder) + self.document.walkabout(visitor) + visitor.finish() + for attr in self.visitor_attributes: + setattr(self, attr, getattr(visitor, attr)) + + +class TexinfoTranslator(nodes.NodeVisitor): + + default_elements = { + 'filename': '', + 'title': '', + 'paragraphindent': 2, + 'exampleindent': 4, + 'direntry': '', + 'preamble': '', + 'body': '', + } + + def __init__(self, document, builder): + nodes.NodeVisitor.__init__(self, document) + self.builder = builder + self.init_settings() + + self.written_ids = set() # node names and anchors in output + self.referenced_ids = set() # node names and anchors that should + # be in output + self.node_names = {} # node name --> node's name to display + self.node_menus = {} # node name --> node's menu entries + self.rellinks = {} # node name --> (next, previous, up) + + self.collect_node_names() + self.collect_node_menus() + self.collect_rellinks() + + self.short_ids = {} + self.body = [] + self.previous_section = None + self.section_level = 0 + self.seen_title = False + self.next_section_targets = [] + self.escape_newlines = 0 + self.curfilestack = [] + + def finish(self): + while self.referenced_ids: + # Handle xrefs with missing anchors + r = self.referenced_ids.pop() + if r not in self.written_ids: + self.document.reporter.info( + "Unknown cross-reference target: `%s'" % r) + self.add_text('@anchor{%s}@w{%s}\n' % (r, ' ' * 30)) + self.fragment = ''.join(self.body).strip() + '\n' + self.elements['body'] = self.fragment + self.output = TEMPLATE % self.elements + + + ## Helper routines + + def init_settings(self): + settings = self.settings = self.document.settings + elements = self.elements = self.default_elements.copy() + elements.update({ + # if empty, the title is set to the first section title + 'title': settings.title, + 'author': escape_arg(settings.author), + # if empty, use basename of input file + 'filename': settings.texinfo_filename, + }) + # Title + title = elements['title'] + if not title: + title = self.document.next_node(nodes.title) + title = (title and title.astext()) or '' + elements['title'] = escape_id(title) or '' + # Filename + if not elements['filename']: + elements['filename'] = self.document.get('source') or 'untitled' + if elements['filename'][-4:] in ('.txt', '.rst'): + elements['filename'] = elements['filename'][:-4] + elements['filename'] += '.info' + # Direntry + if settings.texinfo_dir_entry: + elements['direntry'] = ('@dircategory %s\n' + '@direntry\n' + '* %s: (%s). %s\n' + '@end direntry\n') % ( + escape_id(settings.texinfo_dir_category), + escape_id(settings.texinfo_dir_entry), + elements['filename'], + escape_arg(settings.texinfo_dir_description)) + # allow the user to override them all + elements.update(settings.texinfo_elements) + + def collect_node_names(self): + """Generates a unique id for each section. + + Assigns the attribute ``node_name`` to each section.""" + self.document['node_name'] = 'Top' + self.node_names['Top'] = 'Top' + self.written_ids.update(('Top', 'top')) + + for section in self.document.traverse(nodes.section): + title = section.next_node(nodes.Titular) + name = (title and title.astext()) or '' + node_id = name = escape_id(name) or '' + assert node_id and name + nth, suffix = 1, '' + while (node_id + suffix).lower() in self.written_ids: + nth += 1 + suffix = '<%s>' % nth + node_id += suffix + assert node_id not in self.node_names + assert node_id not in self.written_ids + assert node_id.lower() not in self.written_ids + section['node_name'] = node_id + self.node_names[node_id] = name + self.written_ids.update((node_id, node_id.lower())) + + def collect_node_menus(self): + """Collect the menu entries for each "node" section.""" + node_menus = self.node_menus + for node in ([self.document] + + self.document.traverse(nodes.section)): + assert 'node_name' in node and node['node_name'] + entries = tuple(s['node_name'] + for s in find_subsections(node)) + node_menus[node['node_name']] = entries + # Try to find a suitable "Top" node + title = self.document.next_node(nodes.title) + top = (title and title.parent) or self.document + if not isinstance(top, (nodes.document, nodes.section)): + top = self.document + if top is not self.document: + entries = node_menus[top['node_name']] + entries += node_menus['Top'][1:] + node_menus['Top'] = entries + del node_menus[top['node_name']] + top['node_name'] = 'Top' + + def collect_rellinks(self): + """Collect the relative links (next, previous, up) for each "node".""" + rellinks = self.rellinks + node_menus = self.node_menus + for id, entries in node_menus.items(): + rellinks[id] = ['', '', ''] + # Up's + for id, entries in node_menus.items(): + for e in entries: + rellinks[e][2] = id + # Next's and prev's + for id, entries in node_menus.items(): + for i, id in enumerate(entries): + # First child's prev is empty + if i != 0: + rellinks[id][1] = entries[i-1] + # Last child's next is empty + if i != len(entries) - 1: + rellinks[id][0] = entries[i+1] + # Top's next is its first child + try: + first = node_menus['Top'][0] + except IndexError: + pass + else: + rellinks['Top'][0] = first + rellinks[first][1] = 'Top' + + def add_text(self, text, fresh=False): + """Add some text to the output. + + Optional argument ``fresh`` means to insert a newline before + the text if the last character out was not a newline.""" + if fresh: + if self.body and not self.body[-1].endswith('\n'): + self.body.append('\n') + self.body.append(text) + + def rstrip(self): + """Strip trailing whitespace from the current output.""" + while self.body and not self.body[-1].strip(): + del self.body[-1] + if not self.body: + return + self.body[-1] = self.body[-1].rstrip() + + def add_menu_entries(self, entries): + for entry in entries: + name = self.node_names[entry] + if name == entry: + self.add_text('* %s::\n' % name, fresh=1) + else: + self.add_text('* %s: %s.\n' % (name, entry), fresh=1) + + def add_menu(self, section, master=False): + entries = self.node_menus[section['node_name']] + if not entries: + return + self.add_text('\n@menu\n') + self.add_menu_entries(entries) + if master: + # Write the "detailed menu" + started_detail = False + for entry in entries: + subentries = self.node_menus[entry] + if not subentries: + continue + if not started_detail: + started_detail = True + self.add_text('\n@detailmenu\n' + ' --- The Detailed Node Listing ---\n') + self.add_text('\n%s\n\n' % self.node_names[entry]) + self.add_menu_entries(subentries) + if started_detail: + self.rstrip() + self.add_text('\n@end detailmenu\n') + self.rstrip() + self.add_text('\n@end menu\n\n') + + + ## xref handling + + def get_short_id(self, id): + """Return a shorter 'id' associated with ``id``.""" + # Shorter ids improve paragraph filling in places + # that the id is hidden by Emacs. + try: + sid = self.short_ids[id] + except KeyError: + sid = hex(len(self.short_ids))[2:] + self.short_ids[id] = sid + return sid + + def add_anchor(self, id, msg_node=None): + # Anchors can be referenced by their original id + # or by the generated shortened id + id = escape_id(id).lower() + ids = (self.get_short_id(id), id) + for id in ids: + if id not in self.written_ids: + self.add_text('@anchor{%s}' % id) + self.written_ids.add(id) + + def add_xref(self, ref, name, node): + ref = self.get_short_id(escape_id(ref).lower()) + name = ' '.join(name.split()).strip() + if not name or ref == name: + self.add_text('@pxref{%s}' % ref) + else: + self.add_text('@pxref{%s,%s}' % (ref, name)) + self.referenced_ids.add(ref) + + ## Visiting + + def visit_document(self, node): + pass + def depart_document(self, node): + pass + + def visit_Text(self, node): + s = escape(node.astext()) + if self.escape_newlines: + s = s.replace('\n', ' ') + self.add_text(s) + def depart_Text(self, node): + pass + + def visit_section(self, node): + self.next_section_targets.extend(node.get('ids', [])) + if not self.seen_title: + return + if self.previous_section: + self.add_menu(self.previous_section) + else: + self.add_menu(self.document, master=True) + + node_name = node['node_name'] + pointers = tuple([node_name] + self.rellinks[node_name]) + self.add_text('\n@node %s,%s,%s,%s\n' % pointers) + if node_name != node_name.lower(): + self.add_text('@anchor{%s}' % node_name.lower()) + for id in self.next_section_targets: + self.add_anchor(id, node) + + self.next_section_targets = [] + self.previous_section = node + self.section_level += 1 + + def depart_section(self, node): + self.section_level -= 1 + + headings = ( + '@unnumbered', + '@chapter', + '@section', + '@subsection', + '@subsubsection', + ) + + rubrics = ( + '@heading', + '@subheading', + '@subsubheading', + ) + + def visit_title(self, node): + if not self.seen_title: + self.seen_title = 1 + raise nodes.SkipNode + parent = node.parent + if isinstance(parent, nodes.table): + return + if isinstance(parent, nodes.Admonition): + raise nodes.SkipNode + elif isinstance(parent, nodes.sidebar): + self.visit_rubric(node) + elif isinstance(parent, nodes.topic): + raise nodes.SkipNode + elif not isinstance(parent, nodes.section): + self.document.reporter.warning( + 'encountered title node not in section, topic, table, ' + 'admonition or sidebar', base_node=node) + self.visit_rubric(node) + else: + try: + heading = self.headings[self.section_level] + except IndexError: + heading = self.headings[-1] + self.add_text('%s ' % heading, fresh=1) + + def depart_title(self, node): + self.add_text('', fresh=1) + + def visit_rubric(self, node): + try: + rubric = self.rubrics[self.section_level] + except IndexError: + rubric = self.rubrics[-1] + self.add_text('%s ' % rubric, fresh=1) + def depart_rubric(self, node): + self.add_text('', fresh=1) + + def visit_subtitle(self, node): + self.add_text('\n\n@noindent\n') + def depart_subtitle(self, node): + self.add_text('\n\n') + + ## References + + def visit_target(self, node): + if node.get('ids'): + self.add_anchor(node['ids'][0], node) + elif node.get('refid'): + # Section targets need to go after the start of the section. + next = node.next_node(ascend=1, siblings=1) + while isinstance(next, nodes.target): + next = next.next_node(ascend=1, siblings=1) + if isinstance(next, nodes.section): + self.next_section_targets.append(node['refid']) + return + self.add_anchor(node['refid'], node) + elif node.get('refuri'): + pass + else: + self.document.reporter.error("Unknown target type: %r" % node) + + def visit_reference(self, node): + if isinstance(node.parent, nodes.title): + return + if isinstance(node[0], nodes.image): + return + if isinstance(node.parent, addnodes.desc_type): + return + name = node.get('name', node.astext()).strip() + if node.get('refid'): + self.add_xref(escape_id(node['refid']), + escape_id(name), node) + raise nodes.SkipNode + if not node.get('refuri'): + self.document.reporter.error("Unknown reference type: %s" % node) + return + uri = node['refuri'] + if uri.startswith('#'): + self.add_xref(escape_id(uri[1:]), escape_id(name), node) + elif uri.startswith('%'): + id = uri[1:] + if '#' in id: + src, id = uri[1:].split('#', 1) + assert '#' not in id + self.add_xref(escape_id(id), escape_id(name), node) + elif uri.startswith('mailto:'): + uri = escape_arg(uri[7:]) + name = escape_arg(name) + if not name or name == uri: + self.add_text('@email{%s}' % uri) + else: + self.add_text('@email{%s,%s}' % (uri, name)) + elif uri.startswith('info:'): + uri = uri[5:].replace('_', ' ') + uri = escape_arg(uri) + id = 'Top' + if '#' in uri: + uri, id = uri.split('#', 1) + id = escape_id(id) + name = escape_id(name) + if name == id: + self.add_text('@pxref{%s,,,%s}' % (id, uri)) + else: + self.add_text('@pxref{%s,,%s,%s}' % (id, name, uri)) + else: + uri = escape_arg(uri) + name = escape_arg(name) + if not name or uri == name: + self.add_text('@indicateurl{%s}' % uri) + else: + self.add_text('@uref{%s,%s}' % (uri, name)) + raise nodes.SkipNode + + def depart_reference(self, node): + pass + + def visit_title_reference(self, node): + text = node.astext() + self.add_text('@cite{%s}' % escape_arg(text)) + raise nodes.SkipNode + def visit_title_reference(self, node): + pass + + ## Blocks + + def visit_paragraph(self, node): + if 'continued' in node or isinstance(node.parent, nodes.compound): + self.add_text('@noindent\n', fresh=1) + def depart_paragraph(self, node): + self.add_text('\n\n') + + def visit_block_quote(self, node): + self.rstrip() + self.add_text('\n\n@quotation\n') + def depart_block_quote(self, node): + self.rstrip() + self.add_text('\n@end quotation\n\n') + + def visit_literal_block(self, node): + self.rstrip() + self.add_text('\n\n@example\n') + def depart_literal_block(self, node): + self.rstrip() + self.add_text('\n@end example\n\n' + '@noindent\n') + + visit_doctest_block = visit_literal_block + depart_doctest_block = depart_literal_block + + def visit_line_block(self, node): + self.add_text('@display\n', fresh=1) + def depart_line_block(self, node): + self.add_text('@end display\n', fresh=1) + + def visit_line(self, node): + self.rstrip() + self.add_text('\n') + self.escape_newlines += 1 + def depart_line(self, node): + self.add_text('@w{ }\n') + self.escape_newlines -= 1 + + ## Inline + + def visit_strong(self, node): + self.add_text('@strong{') + def depart_strong(self, node): + self.add_text('}') + + def visit_emphasis(self, node): + self.add_text('@emph{') + def depart_emphasis(self, node): + self.add_text('}') + + def visit_literal(self, node): + self.add_text('@code{') + def depart_literal(self, node): + self.add_text('}') + + def visit_superscript(self, node): + self.add_text('@w{^') + def depart_superscript(self, node): + self.add_text('}') + + def visit_subscript(self, node): + self.add_text('@w{[') + def depart_subscript(self, node): + self.add_text(']}') + + ## Footnotes + + def visit_footnote(self, node): + self.visit_block_quote(node) + def depart_footnote(self, node): + self.depart_block_quote(node) + + def visit_footnote_reference(self, node): + self.add_text('@w{(') + def depart_footnote_reference(self, node): + self.add_text(')}') + + visit_citation = visit_footnote + depart_citation = depart_footnote + + def visit_citation_reference(self, node): + self.add_text('@w{[') + def depart_citation_reference(self, node): + self.add_text(']}') + + ## Lists + + def visit_bullet_list(self, node): + bullet = node.get('bullet', '*') + self.rstrip() + self.add_text('\n\n@itemize %s\n' % bullet) + def depart_bullet_list(self, node): + self.rstrip() + self.add_text('\n@end itemize\n\n') + + def visit_enumerated_list(self, node): + # Doesn't support Roman numerals + enum = node.get('enumtype', 'arabic') + starters = {'arabic': '', + 'loweralpha': 'a', + 'upperalpha': 'A',} + start = node.get('start', starters.get(enum, '')) + self.rstrip() + self.add_text('\n\n@enumerate %s\n' % start) + def depart_enumerated_list(self, node): + self.rstrip() + self.add_text('\n@end enumerate\n\n') + + def visit_list_item(self, node): + self.rstrip() + self.add_text('\n@item\n') + def depart_list_item(self, node): + pass + + ## Option List + + def visit_option_list(self, node): + self.add_text('\n@table @option\n') + def depart_option_list(self, node): + self.rstrip() + self.add_text('\n@end table\n\n') + + def visit_option_list_item(self, node): + pass + def depart_option_list_item(self, node): + pass + + def visit_option_group(self, node): + self.at_item_x = '@item' + def depart_option_group(self, node): + pass + + def visit_option(self, node): + self.add_text(self.at_item_x + ' ', fresh=1) + self.at_item_x = '@itemx' + def depart_option(self, node): + pass + + def visit_option_string(self, node): + pass + def depart_option_string(self, node): + pass + + def visit_option_argument(self, node): + self.add_text(node.get('delimiter', ' ')) + def depart_option_argument(self, node): + pass + + def visit_description(self, node): + self.add_text('', fresh=1) + def depart_description(self, node): + pass + + ## Definitions + + def visit_definition_list(self, node): + self.add_text('\n@table @asis\n') + def depart_definition_list(self, node): + self.rstrip() + self.add_text('\n@end table\n\n') + + def visit_definition_list_item(self, node): + self.at_item_x = '@item' + def depart_definition_list_item(self, node): + pass + + def visit_term(self, node): + if node.get('ids') and node['ids'][0]: + self.add_anchor(node['ids'][0], node) + self.add_text(self.at_item_x + ' ', fresh=1) + self.at_item_x = '@itemx' + def depart_term(self, node): + pass + + def visit_classifier(self, node): + self.add_text(' : ') + def depart_classifier(self, node): + pass + + def visit_definition(self, node): + self.add_text('', fresh=1) + def depart_definition(self, node): + pass + + ## Tables + + def visit_table(self, node): + self.entry_sep = '@item' + def depart_table(self, node): + self.rstrip() + self.add_text('\n@end multitable\n\n') + + def visit_tabular_col_spec(self, node): + pass + def depart_tabular_col_spec(self, node): + pass + + def visit_colspec(self, node): + self.colwidths.append(node['colwidth']) + if len(self.colwidths) != self.n_cols: + return + self.add_text('@multitable ', fresh=1) + for i, n in enumerate(self.colwidths): + self.add_text('{%s} ' %('x' * (n+2))) + def depart_colspec(self, node): + pass + + def visit_tgroup(self, node): + self.colwidths = [] + self.n_cols = node['cols'] + def depart_tgroup(self, node): + pass + + def visit_thead(self, node): + self.entry_sep = '@headitem' + def depart_thead(self, node): + pass + + def visit_tbody(self, node): + pass + def depart_tbody(self, node): + pass + + def visit_row(self, node): + pass + def depart_row(self, node): + self.entry_sep = '@item' + + def visit_entry(self, node): + self.rstrip() + self.add_text('\n%s ' % self.entry_sep) + self.entry_sep = '@tab' + def depart_entry(self, node): + for i in xrange(node.get('morecols', 0)): + self.add_text('@tab\n', fresh=1) + self.add_text('', fresh=1) + + ## Field Lists + + def visit_field_list(self, node): + self.add_text('\n@itemize @w\n') + def depart_field_list(self, node): + self.rstrip() + self.add_text('\n@end itemize\n\n') + + def visit_field(self, node): + if not isinstance(node.parent, nodes.field_list): + self.visit_field_list(None) + def depart_field(self, node): + if not isinstance(node.parent, nodes.field_list): + self.depart_field_list(None) + + def visit_field_name(self, node): + self.add_text('@item ', fresh=1) + def depart_field_name(self, node): + self.add_text(':') + + def visit_field_body(self, node): + self.add_text('', fresh=1) + def depart_field_body(self, node): + pass + + ## Admonitions + + def visit_admonition(self, node): + title = escape(node[0].astext()) + self.add_text('\n@cartouche\n' + '@quotation %s\n' % title) + def depart_admonition(self, node): + self.rstrip() + self.add_text('\n@end quotation\n' + '@end cartouche\n\n') + + def _make_visit_admonition(typ): + def visit(self, node): + title = escape(typ) + self.add_text('\n@cartouche\n' + '@quotation %s\n' % title) + return visit + + visit_attention = _make_visit_admonition('Attention') + visit_caution = _make_visit_admonition('Caution') + visit_danger = _make_visit_admonition('Danger') + visit_error = _make_visit_admonition('Error') + visit_important = _make_visit_admonition('Important') + visit_note = _make_visit_admonition('Note') + visit_tip = _make_visit_admonition('Tip') + visit_hint = _make_visit_admonition('Hint') + visit_warning = _make_visit_admonition('Warning') + + depart_attention = depart_admonition + depart_caution = depart_admonition + depart_danger = depart_admonition + depart_error = depart_admonition + depart_important = depart_admonition + depart_note = depart_admonition + depart_tip = depart_admonition + depart_hint = depart_admonition + depart_warning = depart_admonition + + ## Misc + + def visit_docinfo(self, node): + # No 'docinfo_xform' + raise nodes.SkipNode + + def visit_topic(self, node): + # Ignore TOC's since we have to have a "menu" anyway + if 'contents' in node.get('classes', []): + raise nodes.SkipNode + title = node[0] + self.visit_rubric(title) + self.add_text('%s\n' % escape(title.astext())) + self.visit_block_quote(node) + def depart_topic(self, node): + self.depart_block_quote(node) + + def visit_generated(self, node): + raise nodes.SkipNode + def depart_generated(self, node): + pass + + def visit_transition(self, node): + self.add_text('\n\n@noindent\n' + '@exdent @w{%s}\n\n' + '@noindent\n' % ('_' * 70)) + def depart_transition(self, node): + pass + + def visit_attribution(self, node): + self.add_text('@flushright\n', fresh=1) + def depart_attribution(self, node): + self.add_text('@end flushright\n', fresh=1) + + def visit_raw(self, node): + format = node.get('format', '').split() + if 'texinfo' in format or 'texi' in format: + self.add_text(node.astext()) + raise nodes.SkipNode + def depart_raw(self, node): + pass + + def visit_figure(self, node): + self.add_text('\n@float Figure\n') + def depart_figure(self, node): + self.rstrip() + self.add_text('\n@end float\n\n') + + def visit_caption(self, node): + if not isinstance(node.parent, nodes.figure): + self.document.reporter.warning('Caption not inside a figure.', + base_node=node) + return + self.add_text('@caption{', fresh=1) + def depart_caption(self, node): + if isinstance(node.parent, nodes.figure): + self.rstrip() + self.add_text('}\n') + + def visit_image(self, node): + self.add_text('@w{[image]}') + raise nodes.SkipNode + def depart_image(self, node): + pass + + def visit_compound(self, node): + pass + def depart_compound(self, node): + pass + + def visit_sidebar(self, node): + pass + def depart_sidebar(self, node): + pass + + def visit_label(self, node): + self.add_text('@w{(') + def depart_label(self, node): + self.add_text(')} ') + + def visit_legend(self, node): + pass + def depart_legend(self, node): + pass + + def visit_substitution_reference(self, node): + pass + def depart_substitution_reference(self, node): + pass + + def visit_substitution_definition(self, node): + raise nodes.SkipNode + def depart_substitution_definition(self, node): + pass + + def visit_system_message(self, node): + self.add_text('\n@format\n' + '---------- SYSTEM MESSAGE -----------\n') + def depart_system_message(self, node): + self.rstrip() + self.add_text('\n------------------------------------\n' + '@end format\n') + + def visit_comment(self, node): + for line in node.astext().splitlines(): + self.add_text('@c %s\n' % line, fresh=1) + raise nodes.SkipNode + + def visit_problematic(self, node): + self.add_text('>') + def depart_problematic(self, node): + self.add_text('<') + + def unimplemented_visit(self, node): + self.document.reporter.error("Unimplemented node type: `%s'" + % node.__class__.__name__, base_node=node) + + def unknown_visit(self, node): + self.document.reporter.error("Unknown node type: `%s'" + % node.__class__.__name__, base_node=node) + def unknown_departure(self, node): + pass + + ### Sphinx specific + + def visit_productionlist(self, node): + self.visit_literal_block(None) + names = [] + for production in node: + names.append(production['tokenname']) + maxlen = max(len(name) for name in names) + for production in node: + if production['tokenname']: + s = production['tokenname'].ljust(maxlen) + ' ::=' + lastname = production['tokenname'] + else: + s = '%s ' % (' '*len(lastname)) + self.add_text(escape(s)) + self.add_text(escape(production.astext() + '\n')) + self.depart_literal_block(None) + raise nodes.SkipNode + def depart_productionlist(self, node): + pass + + def visit_literal_emphasis(self, node): + self.add_text('@code{') + def depart_literal_emphasis(self, node): + self.add_text('}') + + def visit_module(self, node): + modname = escape_id(node['modname']) + self.add_anchor(modname, node) + + def visit_index(self, node): + # Throws off table alignment + if isinstance(node.parent, nodes.term): + return + for entry in node['entries']: + typ, text, tid, text2 = entry + text = text.replace('!', ' ').replace(';', ' ') + text = escape_id(text) + self.add_text('@geindex %s\n' % text, fresh=1) + + def visit_autosummary_table(self, node): + pass + def depart_autosummary_table(self, node): + pass + + def visit_todo_node(self, node): + self.visit_transition(node) + self.visit_admonition(node) + def depart_todo_node(self, node): + self.depart_admonition(node) + self.visit_transition(node) + + def visit_refcount(self, node): + self.add_text('\n') + def depart_refcount(self, node): + self.add_text('\n\n') + + def visit_versionmodified(self, node): + intro = versionlabels[node['type']] % node['version'] + if node.children: + intro += ': ' + else: + intro += '.' + self.add_text('%s' % escape(intro), fresh=1) + def depart_versionmodified(self, node): + self.rstrip() + self.add_text('\n\n', fresh=1) + + def visit_start_of_file(self, node): + self.curfilestack.append(node.get('docname', '')) + if node.get('docname'): + self.next_section_targets.append(node['docname']) + def depart_start_of_file(self, node): + self.curfilestack.pop() + + def visit_centered(self, node): + txt = escape_arg(node.astext()) + self.add_text('@center %s\n' % txt, fresh=1) + raise nodes.SkipNode + def depart_centered(self, node): + pass + + def visit_seealso(self, node): + pass + def depart_seealso(self, node): + pass + + def visit_meta(self, node): + raise nodes.SkipNode + def depart_meta(self, node): + pass + + def visit_glossary(self, node): + pass + def depart_glossary(self, node): + pass + + def visit_acks(self, node): + pass + def depart_acks(self, node): + pass + + def visit_highlightlang(self, node): + pass + def depart_highlightlang(self, node): + pass + + ## Desc + + desc_map = { + 'function' : 'Function', + 'class': 'Class', + 'method': 'Method', + 'classmethod': 'Class Method', + 'staticmethod': 'Static Method', + 'exception': 'Exception', + 'data': 'Data', + 'attribute': 'Attribute', + 'opcode': 'Opcode', + 'cfunction': 'C Function', + 'cmember': 'C Member', + 'cmacro': 'C Macro', + 'ctype': 'C Type', + 'cvar': 'C Variable', + 'cmdoption': 'Option', + 'describe': 'Description', + } + + def visit_desc(self, node): + self.at_deffnx = '@deffn' + def depart_desc(self, node): + self.rstrip() + self.add_text('@end deffn\n\n', fresh=1) + def visit_desc_signature(self, node): + self.desctype = node.parent['desctype'].strip() + if self.desctype != 'describe' and node['ids']: + self.add_anchor(node['ids'][0], node) + typ = self.desc_map.get(self.desctype, self.desctype) + self.add_text('%s {%s} ' % (self.at_deffnx, escape_arg(typ)), fresh=1) + self.at_deffnx = '@deffnx' + def depart_desc_signature(self, node): + self.add_text("", fresh=1) + + def visit_desc_name(self, node): + pass + def depart_desc_name(self, node): + pass + + 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.add_text(' -> ') + def depart_desc_returns(self, node): + pass + + def visit_desc_parameterlist(self, node): + self.add_text(' (') + self.first_param = 1 + def depart_desc_parameterlist(self, node): + self.add_text(')') + + def visit_desc_parameter(self, node): + if not self.first_param: + self.add_text(', ') + else: + self.first_param = 0 + self.add_text(escape(node.astext())) + raise nodes.SkipNode + def depart_desc_parameter(self, node): + pass + + def visit_desc_optional(self, node): + self.add_text('[') + def depart_desc_optional(self, node): + self.add_text(']') + + def visit_desc_annotation(self, node): + raise nodes.SkipNode + def depart_desc_annotation(self, node): + pass + + def visit_desc_content(self, node): + self.add_text("", fresh=1) + def depart_desc_content(self, node): + pass diff --git a/tests/root/conf.py b/tests/root/conf.py index 89804a30e..b97ddfcc1 100644 --- a/tests/root/conf.py +++ b/tests/root/conf.py @@ -49,6 +49,11 @@ latex_documents = [ latex_additional_files = ['svgimg.svg'] +texinfo_documents = [ + ('contents', 'SphinxTests', 'Sphinx Tests', + 'Georg Brandl \\and someone else', 'Sphinx Testing', 'Miscellaneous'), +] + value_from_conf_py = 84 coverage_c_path = ['special/*.h'] diff --git a/tests/test_build_texinfo.py b/tests/test_build_texinfo.py new file mode 100644 index 000000000..33dbcc91d --- /dev/null +++ b/tests/test_build_texinfo.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +""" + test_build_texinfo + ~~~~~~~~~~~~~~~~~~ + + Test the build process with Texinfo builder with the test root. + + :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import os +import sys +from StringIO import StringIO +from subprocess import Popen, PIPE + +from sphinx.writers.texinfo import TexinfoTranslator + +from util import * +from test_build_html import ENV_WARNINGS + + +def teardown_module(): + (test_root / '_build').rmtree(True) + + +texinfo_warnfile = StringIO() + +if sys.version_info >= (3, 0): + TEXINFO_WARNINGS = remove_unicode_literals(TEXINFO_WARNINGS) + + +@with_app(buildername='texinfo', warning=texinfo_warnfile, cleanenv=True) +def test_texinfo(app): + app.builder.build_all() + # now, try to run makeinfo over it + cwd = os.getcwd() + os.chdir(app.outdir) + try: + try: + p = Popen(['makeinfo', '--no-split', 'SphinxTests.texi'], + stdout=PIPE, stderr=PIPE) + except OSError: + pass # most likely makeinfo was not found + else: + stdout, stderr = p.communicate() + retcode = p.returncode + if retcode != 0: + print stdout + print stderr + del app.cleanup_trees[:] + assert False, 'makeinfo exited with return code %s' % retcode + finally: + os.chdir(cwd) diff --git a/tests/test_quickstart.py b/tests/test_quickstart.py index 541959bd3..3e942744a 100644 --- a/tests/test_quickstart.py +++ b/tests/test_quickstart.py @@ -171,6 +171,10 @@ def test_quickstart_all_answers(tempdir): assert ns['man_pages'] == [ ('contents', 'stasi', u'STASI™ Documentation', [u'Wolfgang Schäuble & G\'Beckstein'], 1)] + assert ns['texinfo_documents'] == [ + ('contents', 'STASI', u'STASI™ Documentation', + u'Wolfgang Schäuble & G\'Beckstein', 'STASI', + 'One line description of project.', 'Miscellaneous'),] assert (tempdir / 'build').isdir() assert (tempdir / 'source' / '.static').isdir()