mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
217 lines
7.1 KiB
Python
217 lines
7.1 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
sphinx.util.nodes
|
|
~~~~~~~~~~~~~~~~~
|
|
|
|
Docutils node-related utility functions for Sphinx.
|
|
|
|
:copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
|
|
:license: BSD, see LICENSE for details.
|
|
"""
|
|
|
|
import re
|
|
|
|
from docutils import nodes
|
|
|
|
from sphinx import addnodes
|
|
from sphinx.locale import pairindextypes
|
|
from sphinx.util.pycompat import class_types
|
|
|
|
|
|
# \x00 means the "<" was backslash-escaped
|
|
explicit_title_re = re.compile(r'^(.+?)\s*(?<!\x00)<(.*?)>$', re.DOTALL)
|
|
caption_ref_re = explicit_title_re # b/w compat alias
|
|
|
|
IGNORED_NODES = (
|
|
nodes.Invisible,
|
|
nodes.Inline,
|
|
nodes.literal_block,
|
|
nodes.doctest_block,
|
|
#XXX there are probably more
|
|
)
|
|
def extract_messages(doctree):
|
|
"""Extract translatable messages from a document tree."""
|
|
for node in doctree.traverse(nodes.TextElement):
|
|
if not node.source:
|
|
continue # built-in message
|
|
if isinstance(node, IGNORED_NODES):
|
|
continue
|
|
# <field_name>orphan</field_name>
|
|
# XXX ignore all metadata (== docinfo)
|
|
if isinstance(node, nodes.field_name) and node.children[0] == 'orphan':
|
|
continue
|
|
|
|
msg = node.rawsource.replace('\n', ' ').strip()
|
|
# XXX nodes rendering empty are likely a bug in sphinx.addnodes
|
|
if msg:
|
|
yield node, msg
|
|
|
|
|
|
def nested_parse_with_titles(state, content, node):
|
|
"""Version of state.nested_parse() that allows titles and does not require
|
|
titles to have the same decoration as the calling document.
|
|
|
|
This is useful when the parsed content comes from a completely different
|
|
context, such as docstrings.
|
|
"""
|
|
# hack around title style bookkeeping
|
|
surrounding_title_styles = state.memo.title_styles
|
|
surrounding_section_level = state.memo.section_level
|
|
state.memo.title_styles = []
|
|
state.memo.section_level = 0
|
|
try:
|
|
return state.nested_parse(content, 0, node, match_titles=1)
|
|
finally:
|
|
state.memo.title_styles = surrounding_title_styles
|
|
state.memo.section_level = surrounding_section_level
|
|
|
|
|
|
def clean_astext(node):
|
|
"""Like node.astext(), but ignore images."""
|
|
node = node.deepcopy()
|
|
for img in node.traverse(nodes.image):
|
|
img['alt'] = ''
|
|
return node.astext()
|
|
|
|
|
|
def split_explicit_title(text):
|
|
"""Split role content into title and target, if given."""
|
|
match = explicit_title_re.match(text)
|
|
if match:
|
|
return True, match.group(1), match.group(2)
|
|
return False, text, text
|
|
|
|
|
|
indextypes = [
|
|
'single', 'pair', 'double', 'triple', 'see', 'seealso',
|
|
]
|
|
|
|
def process_index_entry(entry, targetid):
|
|
indexentries = []
|
|
entry = entry.strip()
|
|
oentry = entry
|
|
main = ''
|
|
if entry.startswith('!'):
|
|
main = 'main'
|
|
entry = entry[1:].lstrip()
|
|
for type in pairindextypes:
|
|
if entry.startswith(type+':'):
|
|
value = entry[len(type)+1:].strip()
|
|
value = pairindextypes[type] + '; ' + value
|
|
indexentries.append(('pair', value, targetid, main))
|
|
break
|
|
else:
|
|
for type in indextypes:
|
|
if entry.startswith(type+':'):
|
|
value = entry[len(type)+1:].strip()
|
|
if type == 'double':
|
|
type = 'pair'
|
|
indexentries.append((type, value, targetid, main))
|
|
break
|
|
# shorthand notation for single entries
|
|
else:
|
|
for value in oentry.split(','):
|
|
value = value.strip()
|
|
main = ''
|
|
if value.startswith('!'):
|
|
main = 'main'
|
|
value = value[1:].lstrip()
|
|
if not value:
|
|
continue
|
|
indexentries.append(('single', value, targetid, main))
|
|
return indexentries
|
|
|
|
|
|
def inline_all_toctrees(builder, docnameset, docname, tree, colorfunc):
|
|
"""Inline all toctrees in the *tree*.
|
|
|
|
Record all docnames in *docnameset*, and output docnames with *colorfunc*.
|
|
"""
|
|
tree = tree.deepcopy()
|
|
for toctreenode in tree.traverse(addnodes.toctree):
|
|
newnodes = []
|
|
includefiles = map(str, toctreenode['includefiles'])
|
|
for includefile in includefiles:
|
|
try:
|
|
builder.info(colorfunc(includefile) + " ", nonl=1)
|
|
subtree = inline_all_toctrees(builder, docnameset, includefile,
|
|
builder.env.get_doctree(includefile), colorfunc)
|
|
docnameset.add(includefile)
|
|
except Exception:
|
|
builder.warn('toctree contains ref to nonexisting '
|
|
'file %r' % includefile,
|
|
builder.env.doc2path(docname))
|
|
else:
|
|
sof = addnodes.start_of_file(docname=includefile)
|
|
sof.children = subtree.children
|
|
newnodes.append(sof)
|
|
toctreenode.parent.replace(toctreenode, newnodes)
|
|
return tree
|
|
|
|
|
|
def make_refnode(builder, fromdocname, todocname, targetid, child, title=None):
|
|
"""Shortcut to create a reference node."""
|
|
node = nodes.reference('', '', internal=True)
|
|
if fromdocname == todocname:
|
|
node['refid'] = targetid
|
|
else:
|
|
node['refuri'] = (builder.get_relative_uri(fromdocname, todocname)
|
|
+ '#' + targetid)
|
|
if title:
|
|
node['reftitle'] = title
|
|
node.append(child)
|
|
return node
|
|
|
|
# monkey-patch Node.traverse to get more speed
|
|
# traverse() is called so many times during a build that it saves
|
|
# on average 20-25% overall build time!
|
|
|
|
def _all_traverse(self, result):
|
|
"""Version of Node.traverse() that doesn't need a condition."""
|
|
result.append(self)
|
|
for child in self.children:
|
|
child._all_traverse(result)
|
|
return result
|
|
|
|
def _fast_traverse(self, cls, result):
|
|
"""Version of Node.traverse() that only supports instance checks."""
|
|
if isinstance(self, cls):
|
|
result.append(self)
|
|
for child in self.children:
|
|
child._fast_traverse(cls, result)
|
|
return result
|
|
|
|
def _new_traverse(self, condition=None,
|
|
include_self=1, descend=1, siblings=0, ascend=0):
|
|
if include_self and descend and not siblings and not ascend:
|
|
if condition is None:
|
|
return self._all_traverse([])
|
|
elif isinstance(condition, class_types):
|
|
return self._fast_traverse(condition, [])
|
|
return self._old_traverse(condition, include_self,
|
|
descend, siblings, ascend)
|
|
|
|
nodes.Node._old_traverse = nodes.Node.traverse
|
|
nodes.Node._all_traverse = _all_traverse
|
|
nodes.Node._fast_traverse = _fast_traverse
|
|
nodes.Node.traverse = _new_traverse
|
|
|
|
# monkey-patch Node.__contains__ to get consistent "in" operator behavior
|
|
# across docutils versions
|
|
|
|
def _new_contains(self, key):
|
|
# support both membership test for children and attributes
|
|
# (has_key is translated to "in" by 2to3)
|
|
if isinstance(key, basestring):
|
|
return key in self.attributes
|
|
return key in self.children
|
|
|
|
nodes.Node.__contains__ = _new_contains
|
|
|
|
# monkey-patch Element.copy to copy the rawsource
|
|
|
|
def _new_copy(self):
|
|
return self.__class__(self.rawsource, **self.attributes)
|
|
|
|
nodes.Element.copy = _new_copy
|