#10: implement HTML section numbering.

This commit is contained in:
Georg Brandl
2009-02-22 15:22:23 +01:00
parent 69aab57297
commit 7792e27030
10 changed files with 134 additions and 13 deletions

View File

@@ -41,6 +41,9 @@ New features added
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.

View File

@@ -77,6 +77,18 @@ tables of contents. The ``toctree`` directive is the central element.
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::
@@ -124,7 +136,8 @@ tables of contents. The ``toctree`` directive is the central element.
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

View File

@@ -9,10 +9,14 @@ suggest new entries!
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?

View File

@@ -246,7 +246,7 @@ class Builder(object):
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)
@@ -257,7 +257,7 @@ class Builder(object):
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:
@@ -265,6 +265,16 @@ class Builder(object):
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)
@@ -282,7 +292,7 @@ class Builder(object):
# 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()

View File

@@ -79,6 +79,9 @@ class StandaloneHTMLBuilder(Builder):
# 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()
@@ -336,6 +339,7 @@ class StandaloneHTMLBuilder(Builder):
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')

View File

@@ -32,6 +32,7 @@ class TocTree(Directive):
'maxdepth': int,
'glob': directives.flag,
'hidden': directives.flag,
'numbered': directives.flag,
}
def run(self):
@@ -93,6 +94,7 @@ class TocTree(Directive):
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

View File

@@ -277,11 +277,13 @@ class BuildEnvironment:
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
@@ -341,11 +343,13 @@ class BuildEnvironment:
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)
@@ -502,7 +506,8 @@ class BuildEnvironment:
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)
@@ -514,6 +519,11 @@ class BuildEnvironment:
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 --------------------------------------------
@@ -824,6 +834,8 @@ class BuildEnvironment:
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
@@ -1315,6 +1327,57 @@ class BuildEnvironment:
# 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 = {}

View File

@@ -158,6 +158,8 @@ class HTMLTranslator(BaseTranslator):
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):
@@ -176,6 +178,17 @@ class HTMLTranslator(BaseTranslator):
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):
@@ -186,6 +199,11 @@ class HTMLTranslator(BaseTranslator):
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):

View File

@@ -9,6 +9,7 @@ Contents:
.. toctree::
:maxdepth: 2
:numbered:
images
subdir/images

View File

@@ -76,19 +76,22 @@ def test_second_update():
# 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