Georg Brandl avatar Georg Brandl committed 633e154

#10: implement HTML section numbering.

Comments (0)

Files changed (10)

     line.  Also, the current builder output format (e.g. "html" or
     "latex") is always a defined tag.
 
+  - #10: Added HTML section numbers, enabled by giving a
+    ``:numbered:`` flag to the ``toctree`` directive.
+
   - The ``literalinclude`` directive now supports several more
     options, to include only parts of a file.
 
    You can also add external links, by giving an HTTP URL instead of a document
    name.
 
+   If you want to have section numbers even in HTML output, give the toctree a
+   ``numbered`` flag option.  For example::
+
+      .. toctree::
+         :numbered:
+
+         foo
+         bar
+
+   Numbering then starts at the heading of ``foo``.  Sub-toctrees are
+   automatically numbered (don't give the ``numbered`` flag to those).
+
    You can use "globbing" in toctree directives, by giving the ``glob`` flag
    option.  All entries are then matched against the list of available
    documents, and matches are inserted into the list alphabetically.  Example::
       Added "globbing" option.
 
    .. versionchanged:: 0.6
-      Added "hidden" option and external links, as well as support for "self".
+      Added "numbered" and "hidden" options as well as external links and
+      support for "self" references.
 
 
 Special names
 How do I...
 -----------
 
+... get section numbers?
+   They are automatic in LaTeX output; for HTML, give a ``:numbered:`` option to
+   the :dir:`toctree` directive where you want to start numbering.
+
 ... customize the look of the built HTML files?
    Use themes, see :doc:`theming`.
 
-... add global substitutions?
+... add global substitutions or includes?
    Add them in the :confval:`rst_epilog` config value.
 
 ... write my own extension?

sphinx/builders/__init__.py

             self.info(bold('building [%s]: ' % self.name), nonl=1)
             self.info(summary)
 
-        updated_docnames = []
+        updated_docnames = set()
         # while reading, collect all warnings from docutils
         warnings = []
         self.env.set_warnfunc(warnings.append)
         self.info(iterator.next())
         for docname in self.status_iterator(iterator, 'reading sources... ',
                                             purple):
-            updated_docnames.append(docname)
+            updated_docnames.add(docname)
             # nothing further to do, the environment has already
             # done the reading
         for warning in warnings:
                 self.warn(warning)
         self.env.set_warnfunc(self.warn)
 
+        doccount = len(updated_docnames)
+        self.info(bold('looking for now-outdated files... '), nonl=1)
+        for docname in self.env.check_dependents(updated_docnames):
+            updated_docnames.add(docname)
+        outdated = len(updated_docnames) - doccount
+        if outdated:
+            self.info('%d found' % outdated)
+        else:
+            self.info('none found')
+
         if updated_docnames:
             # save the environment
             self.info(bold('pickling environment... '), nonl=True)
 
         # another indirection to support builders that don't build
         # files individually
-        self.write(docnames, updated_docnames, method)
+        self.write(docnames, list(updated_docnames), method)
 
         # finish (write static files etc.)
         self.finish()

sphinx/builders/html.py

         # a hash of all config values that, if changed, cause a full rebuild
         self.config_hash = ''
         self.tags_hash = ''
+        # section numbers for headings in the currently visited document
+        self.secnumbers = {}
+
         self.init_templates()
         self.init_highlighter()
         self.init_translator_class()
         destination = StringOutput(encoding='utf-8')
         doctree.settings = self.docsettings
 
+        self.secnumbers = self.env.toc_secnumbers.get(docname, {})
         self.imgpath = relative_uri(self.get_target_uri(docname), '_images')
         self.post_process_images(doctree)
         self.dlpath = relative_uri(self.get_target_uri(docname), '_downloads')

sphinx/directives/other.py

         'maxdepth': int,
         'glob': directives.flag,
         'hidden': directives.flag,
+        'numbered': directives.flag,
     }
 
     def run(self):
         subnode['maxdepth'] = self.options.get('maxdepth', -1)
         subnode['glob'] = glob
         subnode['hidden'] = 'hidden' in self.options
+        subnode['numbered'] = 'numbered' in self.options
         ret.append(subnode)
         return ret
 

sphinx/environment.py

         self.toc_num_entries = {}   # docname -> number of real entries
         # used to determine when to show the TOC
         # in a sidebar (don't show if it's only one item)
+        self.toc_secnumbers = {}    # docname -> dict of sectionid -> number
 
         self.toctree_includes = {}  # docname -> list of toctree includefiles
         self.files_to_rebuild = {}  # docname -> set of files
                                     # (containing its TOCs) to rebuild too
         self.glob_toctrees = set()  # docnames that have :glob: toctrees
+        self.numbered_toctrees = set() # docnames that have :numbered: toctrees
 
         # X-ref target inventory
         self.descrefs = {}          # fullname -> docname, desctype
             self.dependencies.pop(docname, None)
             self.titles.pop(docname, None)
             self.tocs.pop(docname, None)
+            self.toc_secnumbers.pop(docname, None)
             self.toc_num_entries.pop(docname, None)
             self.toctree_includes.pop(docname, None)
             self.filemodules.pop(docname, None)
             self.indexentries.pop(docname, None)
             self.glob_toctrees.discard(docname)
+            self.numbered_toctrees.discard(docname)
             self.images.purge_doc(docname)
             self.dlfiles.purge_doc(docname)
 
             self.clear_doc(docname)
 
         # read all new and changed files
-        for docname in sorted(added | changed):
+        to_read = added | changed
+        for docname in sorted(to_read):
             yield docname
             self.read_doc(docname, app=app)
 
         if app:
             app.emit('env-updated', self)
 
+    def check_dependents(self, already):
+        to_rewrite = self.assign_section_numbers()
+        for docname in to_rewrite:
+            if docname not in already:
+                yield docname
 
     # --------- SINGLE FILE READING --------------------------------------------
 
            file relations from it."""
         if toctreenode['glob']:
             self.glob_toctrees.add(docname)
+        if toctreenode['numbered']:
+            self.numbered_toctrees.add(docname)
         includefiles = toctreenode['includefiles']
         for includefile in includefiles:
             # note that if the included file is rebuilt, this one must be
         # allow custom references to be resolved
         builder.app.emit('doctree-resolved', doctree, fromdocname)
 
+    def assign_section_numbers(self):
+        """Assign a section number to each heading under a numbered toctree."""
+        # a list of all docnames whose section numbers changed
+        rewrite_needed = []
+
+        old_secnumbers = self.toc_secnumbers
+        self.toc_secnumbers = {}
+
+        def _walk_toc(node, secnums, titlenode=None):
+            # titlenode is the title of the document, it will get assigned a
+            # secnumber too, so that it shows up in next/prev/parent rellinks
+            for subnode in node.children:
+                if isinstance(subnode, nodes.bullet_list):
+                    numstack.append(0)
+                    _walk_toc(subnode, secnums, titlenode)
+                    numstack.pop()
+                    titlenode = None
+                elif isinstance(subnode, nodes.list_item):
+                    _walk_toc(subnode, secnums, titlenode)
+                    titlenode = None
+                elif isinstance(subnode, addnodes.compact_paragraph):
+                    numstack[-1] += 1
+                    secnums[subnode[0]['anchorname']] = \
+                        subnode[0]['secnumber'] = tuple(numstack)
+                    if titlenode:
+                        titlenode['secnumber'] = tuple(numstack)
+                        titlenode = None
+                elif isinstance(subnode, addnodes.toctree):
+                    _walk_toctree(subnode)
+
+        def _walk_toctree(toctreenode):
+            for (title, ref) in toctreenode['entries']:
+                if url_re.match(ref) or ref == 'self':
+                    # don't mess with those
+                    continue
+                if ref in self.tocs:
+                    secnums = self.toc_secnumbers[ref] = {}
+                    _walk_toc(self.tocs[ref], secnums, self.titles.get(ref))
+                    if secnums != old_secnumbers.get(ref):
+                        rewrite_needed.append(ref)
+
+        for docname in self.numbered_toctrees:
+            doctree = self.get_doctree(docname)
+            for toctreenode in doctree.traverse(addnodes.toctree):
+                if toctreenode.get('numbered'):
+                    # every numbered toctree gets new numbering
+                    numstack = [0]
+                    _walk_toctree(toctreenode)
+
+        return rewrite_needed
+
     def create_index(self, builder, _fixre=re.compile(r'(.*) ([(][^()]*[)])')):
         """Create the real index from the collected index entries."""
         new = {}

sphinx/writers/html.py

                 return
             self.body[-1] = '<a title="%s"' % self.attval(node['reftitle']) + \
                             starttag[2:]
+        if node.hasattr('secnumber'):
+            self.body.append('%s. ' % '.'.join(map(str, node['secnumber'])))
 
     # overwritten -- we don't want source comments to show up in the HTML
     def visit_comment(self, node):
     def depart_seealso(self, node):
         self.depart_admonition(node)
 
+    def add_secnumber(self, node):
+        if node.hasattr('secnumber'):
+            self.body.append('.'.join(map(str, node['secnumber'])) + '. ')
+        elif isinstance(node.parent, nodes.section):
+            anchorname = '#' + node.parent['ids'][0]
+            if anchorname not in self.builder.secnumbers:
+                anchorname = ''  # try first heading which has no anchor
+            if anchorname in self.builder.secnumbers:
+                numbers = self.builder.secnumbers[anchorname]
+                self.body.append('.'.join(map(str, numbers)) + '. ')
+
     # overwritten for docutils 0.4
     if hasattr(BaseTranslator, 'start_tag_with_title'):
         def visit_section(self, node):
         def visit_title(self, node):
             # don't move the id attribute inside the <h> tag
             BaseTranslator.visit_title(self, node, move_ids=0)
+            self.add_secnumber(node)
+    else:
+        def visit_title(self, node):
+            BaseTranslator.visit_title(self, node)
+            self.add_secnumber(node)
 
     # overwritten
     def visit_literal_block(self, node):

tests/root/contents.txt

 
 .. toctree::
    :maxdepth: 2
+   :numbered:
 
    images
    subdir/images

tests/test_env.py

     # delete, add and "edit" (change saved mtime) some files and update again
     env.all_docs['contents'] = 0
     root = path(app.srcdir)
-    (root / 'images.txt').unlink()
+    # important: using "autodoc" because it is the last one to be included in
+    # the contents.txt toctree; otherwise section numbers would shift
+    (root / 'autodoc.txt').unlink()
     (root / 'new.txt').write_text('New file\n========\n')
     it = env.update(app.config, app.srcdir, app.doctreedir, app)
     msg = it.next()
-    assert '1 added, 2 changed, 1 removed' in msg
+    assert '1 added, 3 changed, 1 removed' in msg
     docnames = set()
     for docname in it:
         docnames.add(docname)
-    # "includes" is in there because it contains a reference to a nonexisting
-    # downloadable file, which is given another chance to exist
-    assert docnames == set(['contents', 'new', 'includes'])
-    assert 'images' not in env.all_docs
-    assert 'images' not in env.found_docs
+    # "includes" and "images" are in there because they contain references
+    # to nonexisting downloadable or image files, which are given another
+    # chance to exist
+    assert docnames == set(['contents', 'new', 'includes', 'images'])
+    assert 'autodoc' not in env.all_docs
+    assert 'autodoc' not in env.found_docs
 
 def test_object_inventory():
     refs = env.descrefs
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.